From b4c92cdcbad66ea08e22dbc488cbfda3b9d9ba41 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Mon, 8 Jan 2024 21:03:18 +0000 Subject: [PATCH 01/18] dot files --- .azuredevops/pipelines/AzGovViz.pipeline.yml | 4 ++-- .azuredevops/pipelines/AzGovViz.variables.yml | 6 +++--- .github/workflows/AzGovViz.yml | 2 +- .github/workflows/AzGovViz_OIDC.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.azuredevops/pipelines/AzGovViz.pipeline.yml b/.azuredevops/pipelines/AzGovViz.pipeline.yml index 713598ba..a54545d8 100644 --- a/.azuredevops/pipelines/AzGovViz.pipeline.yml +++ b/.azuredevops/pipelines/AzGovViz.pipeline.yml @@ -20,7 +20,7 @@ schedules: - master #CHECK branch 'master' is applicable? - delete me :) #Running AzOps? Run Azure Governance Visualizer after 'AzOps - Push' .. -#AzOps Accellerator https://github.com/Azure/AzOps-Accelerator +#AzOps Accelerator https://github.com/Azure/AzOps-Accelerator #resources: # pipelines: # - pipeline: 'Push' @@ -188,7 +188,7 @@ jobs: } else { Write-Host "Assuming and insisting that you do not want to publish your tenant insights to the public" - Write-Host "HTML NOT published. Please configure authentication on the webApp ($($env:WEBAPPNAME))" + Write-Host "HTML NOT published. Please configure authentication on the Azure Web App ($($env:WEBAPPNAME))." exit 1 } azurePowerShellVersion: latestVersion diff --git a/.azuredevops/pipelines/AzGovViz.variables.yml b/.azuredevops/pipelines/AzGovViz.variables.yml index c70f8daf..415dc88d 100644 --- a/.azuredevops/pipelines/AzGovViz.variables.yml +++ b/.azuredevops/pipelines/AzGovViz.variables.yml @@ -117,12 +117,12 @@ variables: # String | example: value: "westus" value: 'westeurope' - # Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved + # Defines the limit (default=500) of Microsoft Entra group members; For Microsoft Entra groups that have more members than the defined limit, group members will not be resolved - name: AADGroupMembersLimit # Integer | default = 500 | example: value: 333 value: - # Define warning period for Service Principal secret and certificate expiry + # Define warning period for the service principal secret and certificate expiry - name: AADServicePrincipalExpiryWarningDays # Integer | default = 14 | example: value: 21 value: @@ -276,7 +276,7 @@ variables: # Switch | example: value: true value: - # Define the number of script blocks running in parallel. Leveraging PowerShell Core´s parallel capability you can define the ThrottleLimit (default=5) + # Define the number of script blocks running in parallel. Using PowerShell's parallel capability you can define the ThrottleLimit (default=5). - name: ThrottleLimit # Integer | Default = 10 | example: value: 11 value: diff --git a/.github/workflows/AzGovViz.yml b/.github/workflows/AzGovViz.yml index cfee7d7f..ae91cef6 100644 --- a/.github/workflows/AzGovViz.yml +++ b/.github/workflows/AzGovViz.yml @@ -135,7 +135,7 @@ jobs: } else { Write-Host 'Assuming and insisting that you do not want to publish your tenant insights to the public' - Write-Host "HTML NOT published. Please configure authentication on the webApp ($($env:WebAppName))" + Write-Host "HTML NOT published. Please configure authentication on the Azure Web App ($($env:WebAppName))." exit 1 } azPSVersion: "latest" diff --git a/.github/workflows/AzGovViz_OIDC.yml b/.github/workflows/AzGovViz_OIDC.yml index ea216b35..40544585 100644 --- a/.github/workflows/AzGovViz_OIDC.yml +++ b/.github/workflows/AzGovViz_OIDC.yml @@ -144,7 +144,7 @@ jobs: } else { Write-Host 'Assuming and insisting that you do not want to publish your tenant insights to the public' - Write-Host "HTML NOT published. Please configure authentication on the webApp ($($env:WebAppName))" + Write-Host "HTML NOT published. Please configure authentication on the Azure Web App ($($env:WebAppName))." exit 1 } azPSVersion: "latest" \ No newline at end of file From 5e5c516b08b713319d485a2ac61aa1fe1bef563a Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Mon, 8 Jan 2024 23:02:24 +0000 Subject: [PATCH 02/18] restructure setup - console spiked --- contributionGuide.md | 33 +- run-from/azure-devops.md | 272 +++++++++++++++++ run-from/console.md | 139 +++++++++ run-from/github.md | 181 +++++++++++ setup.md | 640 ++------------------------------------- 5 files changed, 627 insertions(+), 638 deletions(-) create mode 100644 run-from/azure-devops.md create mode 100644 run-from/console.md create mode 100644 run-from/github.md diff --git a/contributionGuide.md b/contributionGuide.md index d92770a5..f14d3a47 100644 --- a/contributionGuide.md +++ b/contributionGuide.md @@ -1,18 +1,17 @@ -# Contribution Guide +# Contribution guide -* Fork the repository -* Your working directory is `.\Azure-MG-Sub-Governance-Reporting` - * In the folder `.\pwsh\dev` find the function you intend to work on, apply your changes - * Edit the file `.\pwsh\dev\devAzGovVizParallel.ps1` - * In the param block update the parameter variable `$ProductVersion` accordingly - * Execute `.\pwsh\dev\buildAzGovVizParallel.ps1` - This step will rebuilt the main `.\pwsh\AzGovVizParallel.ps1` file (incorporating all changes you did in the `.\pwsh\dev` directory) - * Edit the file `.\README.md` - * Update the region `Release history`, replace the changes from the previous release with your changes - * Edit the file `.\history.md` - * Copy over text for the change description you just did for the `.\README.md` - * Execute the newly created AzGovViz version to test if it completes successfully - `.\pwsh\AzGovVizParallel.ps1 -ShowRunIdentifier` - From the very last line of the output take a copy of the __run identifier__ and provide that with the pull request -* Commit your changes -* Create a pull request - * Provide the __run identifier__ in the pull request as a proof of successful test \ No newline at end of file +1. Fork the repository. +1. Change you working directory to `.\Azure-MG-Sub-Governance-Reporting`. +1. In the folder `.\pwsh\dev` find the function you intend to work on and apply your changes. +1. Edit the file `.\pwsh\dev\devAzGovVizParallel.ps1`. + - In the param block update the parameter variable `$ProductVersion` accordingly. +1. Execute `.\pwsh\dev\buildAzGovVizParallel.ps1` - This step will rebuilt the main `.\pwsh\AzGovVizParallel.ps1` file, incorporating all changes you did in the `.\pwsh\dev` directory. +1. Edit the file `.\README.md`. + - Update the region `Release history`, replace the changes from the previous release with your changes. +1. Edit the file `.\history.md`. + - Copy over text for the change description you just did for the `.\README.md`. +1. Execute the newly created AzGovViz version to test if it completes successfully by running `.\pwsh\AzGovVizParallel.ps1 -ShowRunIdentifier`. + - From the very last line of the output copy the __run identifier__, you'll need that when you open your pull request. +1. Commit your changes. +1. Create a pull request + - Provide the __run identifier__ in the pull request as a proof of successful test diff --git a/run-from/azure-devops.md b/run-from/azure-devops.md new file mode 100644 index 00000000..4aacee12 --- /dev/null +++ b/run-from/azure-devops.md @@ -0,0 +1,272 @@ +# Configure and run Azure Governance Visualizer from Azure DevOps + +Also, most steps have both **portal based** ( :computer_mouse: ) and **PowerShell based** ( :keyboard: ) instructions. Use whichever you feel is appropriate for your situation, they both will produce the same results. + +## 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 Azure Governance Visualizer GitHub repository + +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 Azure Governance Visualizer GitHub repository is public - no authorization required + +## Create AzDO Service Connection + +For the pipeline to authenticate and connect to Azure we need to create an AzDO Service Connection which basically is a Service Principal (Application) +There are two options to create the Service Connection: + +* Options + * **Option 1** Create Service Connection´s Service Principal in the Azure Portal + * **Option 2** Create Service Connection in AzDO + +### Create AzDO Service Connection - Option 1 - Create Service Connections Service Principal in the Azure Portal + +#### AzDO supports Open ID Connect - OIDC + +Using OIDC we will not have the requirement to create a secret, nore store it in AzDO - awesome :) + +Quick guide for an app registration: + +**AzDO:** + +* Click on '**Project settings**' (located on the bottom left) +* Under '**Pipelines**' click on '**Service Connections**' +* Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' +* Select Authentication method **Workload Identity federation (manual)** + +![alt text](img/azdo_oidc_0.jpg "Microsoft Entra ID (AAD) Federated credentials") + +Copy away: + +* value of **Issuer** +* value of **Subject identifier** + +![alt text](img/azdo_oidc_1.jpg "Microsoft Entra ID (AAD) Federated credentials; issuer, subject identifier") + +**Microsoft Entra ID (AAD):** + +* In the Azure Portal navigate to 'Microsoft Entra ID (AAD)' +* Click on '**App registrations**' +* Click on '**New registration**' +* Name your application (e.g. 'AzureGovernanceVisualizer_SC') +* Click '**Register**' +* Your App registration has been created +* Under '**Manage**' click on '**Certificates & Secrets**' +* Click on '**Federated credentials**' and '**Add credential**' + +![alt text](img/azdo_aad_oidc_0.jpg "Microsoft Entra ID (AAD) Federated credentials") + +Paste the just copied off + +* value for **Issuer** +* value for **Subject identifier** + +![alt text](img/azdo_aad_oidc_1.jpg "Microsoft Entra ID (AAD) Federated credentials; issuer, subject identifier") + +#### Azure Portal + +* Navigate to 'Microsoft Entra ID (AAD)' +* Click on '**App registrations**' +* Click on '**New registration**' +* 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**' +* Click on '**New client secret**' +* 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 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 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. 'AzureGovernanceVisualizer_SC'). +* Select '**Next**', '**Review + Assign**' + +#### Azure DevOps + +* Click on '**Project settings**' (located on the bottom left) +* Under '**Pipelines**' click on '**Service Connections**' +* Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' +* For the authentication method select '**Service principal (manual)**' and click '**Next**' +* For the '**Scope level**' select '**Management Group**' + * In the field '**Management Group Id**' enter the target Management Group Id + * In the field '**Management Group Name**' enter the target Management Group Name +* Under '**Authentication**' in the field '**Service Principal Id**' enter the '**Application (client) ID**' that you copied away earlier +* For the '**Credential**' select '**Service principal key**', in the field '**Service principal key**' enter the secret that you copied away earlier +* For '**Tenant ID**' enter your Tenant Id +* Click on '**Verify**' +* Under '**Details**' provide your Service Connection with a name and copy away the name as we will need that later when editing the Pipeline YAML file +* For '**Security**' leave the 'Grant access permissions to all pipelines' option checked (optional) +* Click on '**Verify and save**' + +### Create AzDO Service Connection - Option 2 - Create Service Connection in AzDO + +* Click on '**Project settings**' (located on the bottom left) +* Under '**Pipelines**' click on '**Service connections**' +* Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' +* For the authentication method select '**Service principal (automatic)**' and click '**Next**' +* For the '**Scope level**' select '**Management Group**', in the Management Group dropdown select the target Management Group (here the Management Group´s display names will be shown), in the '**Details**' section apply a Service Connection name and optional give it a description and click '**Save**' +* A new window will open, authenticate with your administrative account +* Now the Service Connection has been created + +**Important!** In Azure on the target Management Group scope an '**Owner**' RBAC Role assignment for the Service Connection´s Service Principal has been created automatically (we do however only require a '**Reader**' RBAC Role assignment! we will take corrective action in the next steps) + +## 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 AzDO Service Connection´s Service Principal + +* PowerShell + +```powershell +$objectId = "" +$role = "Reader" +$managementGroupId = "" + +New-AzRoleAssignment ` +-ObjectId $objectId ` +-RoleDefinitionName $role ` +-Scope /providers/Microsoft.Management/managementGroups/$managementGroupId +``` + +* Azure Portal +[Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) + +**Important!** If you have created the AzDO Service Connection in AzDO (Option 2) then you SHOULD remove the automatically created '**Owner**' RBAC Role assignment for the AzDO Service Connection´s Service Principal from the target Management Group + +## Grant permissions in Microsoft Entra ID + +### API permissions + +* Requirements + * To grant API permissions and grant admin consent for the directory, 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)) + +Grant API permissions for the Service Principal´s Application that we created earlier + +* Navigate to 'Microsoft Entra ID (AAD)' +* Click on '**App registrations**' +* Search for the Application that we created earlier and click on it +* Under '**Manage**' click on '**API permissions**' + * Click on '**Add a permissions**' + * Click on '**Microsoft Graph**' + * Click on '**Application permissions**' + * Select the following set of permissions and click '**Add permissions**' + * **Application / Application.Read.All** + * **Group / Group.Read.All** + * **User / User.Read.All** + * **PrivilegedAccess / PrivilegedAccess.Read.AzureResources** + * Click on 'Add a permissions' + * Back in the main '**API permissions**' menu you will find the permissions with status 'Not granted for...'. Click on '**Grant admin consent for _TenantName_**' and confirm by click on '**Yes**' + * Now you will find the permissions with status '**Granted for _TenantName_**' + +Permissions in Microsoft Entra ID (AAD) for App registration: +![alt text](img/aadpermissionsportal_4.jpg "Permissions in Microsoft Entra ID (AAD)") + +## Grant permissions on Azure Governance Visualizer AzDO 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 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 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 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) + * Enter the Management Group Id (ManagementGroupId) +* Click '**Commit**' + +## OPTION 1 (legacy) - Create AzDO Pipeline (.pipelines folder) + +* Click on '**Pipelines**' +* Click on '**New pipeline**' +* Select '**Azure Repos Git**' +* 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**' + +## OPTION 2 (new) - Edit AzDO Variables YAML file (.azuredevops folder) + +>For the '**parameters**' and '**variables**' sections, details about each parameter or variable is documented inline. + +* Click on '**Repos**' +* 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) + * [Optional] Update the '**ExcludedResourceTypesDiagnosticsCapableParameters**' + * [Optional] Update the '**SubscriptionQuotaIdWhitelistParameters**' +* Update the '**Required Variables**' section: + * Replace `` with the Service connection name you copied earlier (ServiceConnection) + * Replace `` with the Management Group Id (ManagementGroupId) +* If needed, update the '**Default Variables**' section +* If needed, update the '**Optional Variables**' section + +### OPTION 2 (new) Create AzDO Pipeline (.azuredevops folder) + +* Click on '**Pipelines**' +* Click on '**New pipeline**' +* Select '**Azure Repos Git**' +* Select the Azure Governance Visualizer repository +* Click on '**Existing Azure Pipelines YAML file**' +* Under '**Path**' select '**/.azuredevops/pipelines/AzGovViz.pipeline.yml**' +* Click ' **Save**' + +## Run the AzDO Pipeline + +* Click on '**Pipelines**' +* Select the Azure Governance Visualizer pipeline +* Click '**Run pipeline**' + +Note: Before the pipeline kicks off it may require you to approve the run (only first time run) + +## Create AzDO Wiki (WikiAsCode) + +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 Azure Governance Visualizer repository +* Select the folder '**wiki**' and click '**OK**' +* Enter a name for the Wiki +* Click '**Publish**' + +## 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. + +There are a few models to do this, the option below is one way to get you started. + +### Prerequisites + +* Deploy a simple webapp on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform +![alt text](img/webapp_create.png "Web App Create") +* Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on +![alt text](img/webapp_configure.png "Web App Configure") +* No need to configure anything, unless your organization policies require you to do so +NOTE: it is a good practice to tag your resource for operational and finance reasons +* In the webapp _Configuration_ add the name of the HTML output file to the _Default Documents_ +![alt text](img/webapp_defaultdocs.png "Web App Default documents") +* Make sure to configure Authentication! +![alt text](img/webapp_authentication.png "Web App Authentication") + +### Configure + +* Assign the Azure DevOps Service Connection´s Service Principal with RBAC Role **Website Contributor** on the Azure Web App +* Edit the `.azuredevops/AzGovViz.variables.yml` file +![alt text](img/webapp_AzDO_yml.png "Azure DevOps YAML variables") diff --git a/run-from/console.md b/run-from/console.md new file mode 100644 index 00000000..936d79da --- /dev/null +++ b/run-from/console.md @@ -0,0 +1,139 @@ + +# Configure and run Azure Governance Visualizer from the console + +When trying out Azure Governance Visualizer for the first time or simply as a one-time evaluation of an Azure tenant, the quickest way to get results is to run it directly from the console. These instructions will get you up and running from a terminal. + +Some steps have both **portal based** ( :computer_mouse: ) and **PowerShell based** ( :keyboard: ) instructions. Use whichever you feel is appropriate for your situation, they both will produce the same results. + +The identity executing this script will need read access on the target management group and some basic Microsoft Entra ID permissions. Follow the instructions below based on the type of user you're executing this as. + +---- + +- Requirements + +Create a '**Reader**' RBAC Role assignment on the target Management Group scope for the identity that shall run Azure Governance Visualizer + +- PowerShell + +```powershell +$objectId = "" +$managementGroupId = "" + +New-AzRoleAssignment ` +-ObjectId $objectId ` +-RoleDefinitionName "Reader" ` +-Scope /providers/Microsoft.Management/managementGroups/$managementGroupId +``` + +- Azure Portal +[Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) + +--- + +## Prerequisites + +The following must be installed on the workstation that will be used to run the scripts: + +- [Git](https://git-scm.com/downloads) +- [PowerShell 7](https://github.com/PowerShell/PowerShell#get-powershell) (minimum supported version 7.0.3) +- [Azure PowerShell](https://learn.microsoft.com/powershell/azure/install-azure-powershell) +- [AzAPICall](https://github.com/JulianHayward/AzAPICall#get--set-azapicall-powershell-module) + +## 1. Validate permissions on user + +:arrow_forward: If your user is a tenant _member user_ and you use your user to complete these instructions, then no additional setup is necessary. This is the most common. And you can :arrow_down_small: continue with [**2. Clone the Azure Governance Visualizer repository**](#2-clone-the-azure-governance-visualizer-repository). + +_- or -_ + +:arrow_forward: However, if your user is tenant _guest user_ and you use your user to complete these instructions, continue to [Set up to execute as a tenant _guest user_](#set-up-to-execute-as-a-tenant-guest-user) to ensure your user is configured properly. + +_- or -_ + +:arrow_forward: If instead you are planning on executing the script as a pre-existing service principal instead of as your user, see [Set up to execute as a _service principal_](#set-up-to-execute-as-a-service-principal) to ensure it is configured properly. + +### Set up to execute as a tenant _guest user_ + +Your user is a guest user in the tenant or there are other hardened restrictions on the tenant, then your user must first be assigned the Microsoft Entra ID role '**Directory readers**'. + +:bulb: [Compare member and guest default permissions](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#compare-member-and-guest-default-permissions) + +:bulb: [Restrict guest access permissions in Microsoft Entra ID](https://docs.microsoft.com/azure/active-directory/enterprise-users/users-restrict-guest-permissions) + +Work with your Microsoft Entra '**Privileged Role Administrator**' or '**Global Administrator**' to assign the '**Directory readers**' [role to your guest account](https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal). + +:arrow_down_small: Continue with [**2. Clone the Azure Governance Visualizer repository**](#2-clone-the-azure-governance-visualizer-repository). + +### Set up to execute as a _service principal_ + +You are planning on executing the script as a service principal instead of as your user. A service principal, by default, has no read permissions on users, groups, and other service principals, therefore you'll need to work with a Microsoft Entra ID administrator to grant additional permissions to the service principal. The following Microsoft Graph API permissions, with admin consent, need to be added: + +- **Application / Application.Read.All** +- **Group / Group.Read.All** +- **User / User.Read.All** +- **PrivilegedAccess / PrivilegedAccess.Read.AzureResources** + +**:computer_mouse: Use the Microsoft Entra admin center to assign permissions to the service principal:** + +To grant API permissions and grant admin consent for the directory, the user performing the following steps must have '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. + +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/). +1. Click on '**App registrations**' +1. Search for the existing application (service principal) +1. Under '**Manage**' click on '**API permissions**' +1. Click on '**Add a permissions**' +1. Click on '**Microsoft Graph**' +1. Click on '**Application permissions**' +1. Select the following set of permissions and click '**Add permissions**' + - **Application / Application.Read.All** + - **Group / Group.Read.All** + - **User / User.Read.All** + - **PrivilegedAccess / PrivilegedAccess.Read.AzureResources** +1. Click on 'Add a permissions' +1. Back in the main '**API permissions**' menu you will find permissions with status 'Not granted for...'. Click on '**Grant admin consent for _TenantName_**' and confirm by click on '**Yes**' + - Now you will find the permissions with status '**Granted for _TenantName_**' + +Permissions and admin consent granted in Microsoft Entra ID for the service principal (App Registration): + +![Permissions in Microsoft Entra ID](../img/aadpermissionsportal_4.jpg) + +## 2. Clone the Azure Governance Visualizer repository + +```powershell +Set-Location "c:\Git" +git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" +``` + +## 2. Authenticate to Azure + +**As your user:** + +```powershell +Connect-AzAccount -TenantId -UseDeviceAuthentication +``` + +**As the configured service principal:** + +Have the '**Application (client) ID**' of the app registration OR '**Application ID**' of the service principal (Enterprise application) and the secret of the app registration at hand. + +```powershell +$pscredential = Get-Credential +Connect-AzAccount -ServicePrincipal -TenantId -Credential $pscredential +``` + +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 + +## 3. Run the Azure Governance Visualizer + +Familiarize yourself with the available [parameters](../README.md#parameters) for Azure Governance Visualizer. + +```powershell +c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId +``` + +If not using the `-OutputPath` parameter, all output will be created in the current directory. The following example will create the output in directory c:\AzGovViz-Output (directory must exist) + +```powershell +c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -OutputPath "c:\AzGovViz-Output" +``` diff --git a/run-from/github.md b/run-from/github.md new file mode 100644 index 00000000..950ca7bb --- /dev/null +++ b/run-from/github.md @@ -0,0 +1,181 @@ + +# Configure and run Azure Governance Visualizer from GitHub + +Also, most steps have both **portal based** ( :computer_mouse: ) and **PowerShell based** ( :keyboard: ) instructions. Use whichever you feel is appropriate for your situation, they both will produce the same results. + +## Create GitHub repository + +Create a 'private' repository + +## Import Code + +Click on 'Import code' + +Use '' as clone URL + +Click on 'Begin import' + +Navigate to your newly created repository +In the folder `./github/workflows` two worklows are available: + +1. [AzGovViz.yml](#azgovviz-yaml) +Use this workflow if you want to store your Application (App registration) secret in GitHub + +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 + +## 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 'Microsoft Entra ID (AAD)' + +* Click on '**App registrations**' +* Click on '**New registration**' +* 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**' +* Click on '**New client secret**' +* 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 (Azure Governance Visualizer YAML) + +In GitHub navigate to 'Settings' + +* Click on 'Secrets' +* Click on 'Actions' +* Click 'New repository secret' + * Name: CREDS + * Value: + +``` +{ + "tenantId": "", + "subscriptionId": "", + "clientId": "", + "clientSecret": "" +} +``` + +### Workflow permissions + +In GitHub navigate to 'Settings' + +* Click on 'Actions' +* Click on 'General' +* Under 'Workflow permissions' select '**Read and write permissions**' +* Click 'Save' + +### 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 Azure Governance Visualizer then enable the `schedule` in the `on` section + +### 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 Azure Governance Visualizer workflow +* Click 'Run workflow' + +## 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 not have the requirement to create a secret, nore store it in GitHub - awesome :) + +* Navigate to 'Microsoft Entra ID (AAD)' +* Click on '**App registrations**' +* Click on '**New registration**' +* 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**' +* Click on '**Federated credentials**' +* Click 'Add credential' +* Select Federation credential scenario 'GitHub Actions deploying Azure Resources' +* 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 Azure Governance Visualizer repository) +* Fill the field 'Name' with a name (e.g. AzureGovernanceVisualizer_GitHub_Actions) +* Click 'Add' + +### Store the credentials in GitHub (Azure Governance Visualizer OIDC YAML) + +In GitHub navigate to 'Settings' + +* Click on 'Secrets' +* Click on 'Actions' +* Click 'New repository secret' +* Create the following three secrets: + * Name: CLIENT_ID + Value: `Application (client) ID` + * Name: TENANT_ID + Value: `Tenant ID` + * Name: SUBSCRIPTION_ID + Value: `Subscription ID` + +### Workflow permissions + +In GitHub navigate to 'Settings' + +* Click on 'Actions' +* Click on 'General' +* Under 'Workflow permissions' select '**Read and write permissions**' +* Click 'Save' + +### 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 Azure Governance Visualizer then enable the `schedule` in the `on` section + +### 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' + +# 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 "Azure Governance Visualizer GitHub Codespaces") + +![alt text](img/codespaces1.png "Azure Governance Visualizer GitHub Codespaces") + +![alt text](img/codespaces2.png "Azure Governance Visualizer GitHub Codespaces") + +![alt text](img/codespaces3.png "Azure Governance Visualizer GitHub Codespaces") + +![alt text](img/codespaces4.png "Azure Governance Visualizer GitHub Codespaces") + +## 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. + +There are a few models to do this, the option below is one way to get you started. + +### Prerequisites + +* Deploy a simple webapp on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform +![alt text](img/webapp_create.png "Web App Create") +* Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on +![alt text](img/webapp_configure.png "Web App Configure") +* No need to configure anything, unless your organization policies require you to do so +NOTE: it is a good practice to tag your resource for operational and finance reasons +* In the webapp _Configuration_ add the name of the HTML output file to the _Default Documents_ +![alt text](img/webapp_defaultdocs.png "Web App Default documents") +* Make sure to configure Authentication! +![alt text](img/webapp_authentication.png "Web App Authentication") + +### Configure + +* Assign the Service Principal used in GitHub with RBAC Role **Website Contributor** on the Azure Web App +* Edit the `.github/workflows/AzGovViz_OIDC.yml` or `.github/workflows/AzGovViz.yml` file +![alt text](img/webapp_GitHub_yml.png "GitHub YAML variables") diff --git a/setup.md b/setup.md index 97f4d6c3..145f11b5 100644 --- a/setup.md +++ b/setup.md @@ -1,637 +1,35 @@ -# Azure Governance Visualizer aka AzGovViz - Setup +# Azure Governance Visualizer (AzGovViz) deployment guide -This guide will help you to setup and run AzGovViz +Follow these steps to deploy the Azure Governance Visualizer. There are three sets of instructions depending on where you wish to execute it. Supported paths are: -* Abbreviations: - * Azure DevOps - AzDO -# 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 Accelerator](#azure-governance-visualizer-from-accelerator) -* [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 Microsoft Entra ID Role - Directory readers](#assign-microsoft-entra-id-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 Connections Service Principal in the Azure Portal](#create-azdo-service-connection---option-1---create-service-connections-service-principal-in-the-azure-portal) - * [AzDO supports Open ID Connect - OIDC](#azdo-supports-open-id-connect---oidc) - * [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 Microsoft Entra ID](#grant-permissions-in-microsoft-entra-id) - * [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) +* Running it ad-hoc from a workstation (console) +* Running it from Azure DevOps +* Running it from GitHub -# Azure Governance Visualizer from Accelerator +No matter which of the three you choose, they all evaluate the same governance concerns and produce the same reporting results, just the execution and reporting environment is distinct. Use whichever environment is best suited for your situation. -* The [Azure Governance Visualizer Accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) provides an easy and fast deployment process that automates the creation and publishing of AzGovViz to an Azure Web Application and provides automation to configuring the pre-requisites for AzGovViz. +### Prerequisites -# Azure Governance Visualizer from Console +* To assign roles as part of the following instructions, 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**'). -## Grant permissions in Azure +### Set up and run Azure Governance Visualizer from the console -* 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__') +To set up local execution of AzGovViz without involving Azure pipelines or GitHub actions. This solution is good for proof of value exploration, local development, etc. It's encouraged that you use Azure DevOps pipelines or GitHub actions for a formal deployment. -Create a '__Reader__' RBAC Role assignment on the target Management Group scope for the identity that shall run Azure Governance Visualizer +Follow the instructions to [Configure and run from the console](./run-from/console.md). -* PowerShell +### Set up and run Azure Governance Visualizer in Azure DevOps -```powershell -$objectId = "" -$role = "Reader" -$managementGroupId = "" +The Azure Governance Visualizer lifecycle can be hosted out of Azure DevOps. This includes automated pipelines, service connections, and even automated wiki generations. This path also optionally supports publishing the generated HTML report to Azure Web Apps. -New-AzRoleAssignment ` --ObjectId $objectId ` --RoleDefinitionName $role ` --Scope /providers/Microsoft.Management/managementGroups/$managementGroupId -``` +Follow the instructions to [Configure and run from Azure DevOps](./run-from/azure-devops.md). -* Azure Portal -[Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) +### Set up and run Azure Governance Visualizer in GitHub -## Execution options +To set up the Azure Governance Visualizer lifecycle, including automated actions, service connections, and GitHub Codespaces. This path also optionally supports publishing the generated HTML report to Azure Web Apps. -### Option 1 - Execute as a Tenant Member User +Follow the instructions to [Configure and run from GitHub](./run-from/github.md). -Proceed with step [__Clone the Azure Governance Visualizer repository__](#clone-the-azure-governance-visualizer-repository) +## Contributions -### Option 2 - Execute as a Tenant Guest User - -If the tenant is hardened (Microsoft Entra ID External Identities / Guest user access = most restrictive) then Guest User must be assigned the Microsoft Entra ID Role '__Directory readers__' - -💡 [Compare member and guest default permissions](https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/fundamentals/users-default-permissions.md#compare-member-and-guest-default-permissions) - -💡 [Restrict Guest permissions](https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions) - -#### Assign Microsoft Entra ID Role - Directory readers - -* 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 Microsoft Entra ID 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 Azure Governance Visualizer repository__](#clone-the-azure-governance-visualizer-repository) - -### Option 3 - Execute as Service Principal - -A Service Principal by default has no read permissions on Users, Groups and Service Principals, therefore we need to grant additional permissions in Microsoft Entra ID - -#### Grant API permissions - -* Requirements - * To grant API permissions and grant admin consent for the directory, 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) - -Grant API permissions for the Service Principal´s Application - -* Navigate to 'Microsoft Entra ID (AAD)' -* Click on '__App registrations__' -* Search for the Application that we created earlier and click on it -* Under '__Manage__' click on '__API permissions__' - * Click on '__Add a permissions__' - * Click on '__Microsoft Graph__' - * Click on '__Application permissions__' - * Select the following set of permissions and click '__Add permissions__' - * __Application / Application.Read.All__ - * __Group / Group.Read.All__ - * __User / User.Read.All__ - * __PrivilegedAccess / PrivilegedAccess.Read.AzureResources__ - * Click on 'Add a permissions' - * Back in the main '__API permissions__' menu you will find permissions with status 'Not granted for...'. Click on '__Grant admin consent for _TenantName___' and confirm by click on '__Yes__' - * Now you will find the permissions with status '__Granted for _TenantName___' - -Permissions in Microsoft Entra ID (AAD) for App registration: -![alt text](img/aadpermissionsportal_4.jpg "Permissions in Microsoft Entra ID (AAD)") - -Proceed with step [__Clone the Azure Governance Visualizer repository__](#clone-the-azure-governance-visualizer-repository) - -## Clone the Azure Governance Visualizer repository - -* Requirements - * 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 - -```powershell -Set-Location "c:\Git" -git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" -``` - -Proceed with step [__Run Azure Governance Visualizer from Console__](#run-azure-governance-visualizer-from-console) - -## Run Azure Governance Visualizer from Console - -### PowerShell & Azure PowerShell modules - -* Requirements - * Requires PowerShell 7 (minimum supported version 7.0.3) - * [Get PowerShell](https://github.com/PowerShell/PowerShell#get-powershell) - * [Installing PowerShell on Windows](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows) - * [Installing PowerShell on Linux](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux) - * Requires PowerShell Az Modules - * Az.Accounts - * [Install the Azure Az PowerShell module](https://docs.microsoft.com/en-us/powershell/azure/install-az-ps) - * AzAPICall - * Running from Console the script will prompt you to confirm installation of the required AzAPICall module version - * AzAPICall resources: - * [![PowerShell Gallery Version (including pre-releases)](https://img.shields.io/powershellgallery/v/AzAPICall?include_prereleases&label=PowerShell%20Gallery)](https://www.powershellgallery.com/packages/AzAPICall) - * [GitHub Repository](https://aka.ms/AzAPICall) - -### Connecting to Azure as User (Member or Guest) - -* PowerShell - -```powershell -Connect-AzAccount -TenantId -UseDeviceAuthentication -``` - -### Connecting to Azure using Service Principal - -Have the '__Application (client) ID__' of the App registration OR '__Application ID__' of the Service Principal (Enterprise Application) and the secret of the App registration at hand - -* PowerShell - -```powershell -$pscredential = Get-Credential -Connect-AzAccount -ServicePrincipal -TenantId -Credential $pscredential -``` - -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 Azure Governance Visualizer - -Familiarize yourself with the available [parameters](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting#parameters) for Azure Governance Visualizer - -* PowerShell - -```powershell -c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -``` - -Note if not using the `-OutputPath` parameter, all outputs will be created in the current directory. The following example will create the outputs in directory c:\AzGovViz-Output (directory must exist) - -* PowerShell - -```powershell -c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -OutputPath "c:\AzGovViz-Output" -``` - -# 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 Azure Governance Visualizer GitHub repository - -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 Azure Governance Visualizer GitHub repository is public - no authorization required - -## Create AzDO Service Connection - -For the pipeline to authenticate and connect to Azure we need to create an AzDO Service Connection which basically is a Service Principal (Application) -There are two options to create the Service Connection: - -* Options - * __Option 1__ Create Service Connection´s Service Principal in the Azure Portal - * __Option 2__ Create Service Connection in AzDO - -### Create AzDO Service Connection - Option 1 - Create Service Connections Service Principal in the Azure Portal - -#### AzDO supports Open ID Connect - OIDC - -Using OIDC we will not have the requirement to create a secret, nore store it in AzDO - awesome :) - -Quick guide for an app registration: - -__AzDO:__ - -* Click on '__Project settings__' (located on the bottom left) -* Under '__Pipelines__' click on '__Service Connections__' -* Click on '__New service connection__' and select the connection/service type '__Azure Resource Manager__' and click '__Next__' -* Select Authentication method __Workload Identity federation (manual)__ - -![alt text](img/azdo_oidc_0.jpg "Microsoft Entra ID (AAD) Federated credentials") - -Copy away: -* value of __Issuer__ -* value of __Subject identifier__ - -![alt text](img/azdo_oidc_1.jpg "Microsoft Entra ID (AAD) Federated credentials; issuer, subject identifier") - -__Microsoft Entra ID (AAD):__ - -* In the Azure Portal navigate to 'Microsoft Entra ID (AAD)' -* Click on '__App registrations__' -* Click on '__New registration__' -* Name your application (e.g. 'AzureGovernanceVisualizer_SC') -* Click '__Register__' -* Your App registration has been created -* Under '__Manage__' click on '__Certificates & Secrets__' -* Click on '__Federated credentials__' and '__Add credential__' - -![alt text](img/azdo_aad_oidc_0.jpg "Microsoft Entra ID (AAD) Federated credentials") - -Paste the just copied off -* value for __Issuer__ -* value for __Subject identifier__ - -![alt text](img/azdo_aad_oidc_1.jpg "Microsoft Entra ID (AAD) Federated credentials; issuer, subject identifier") - -#### Azure Portal -* Navigate to 'Microsoft Entra ID (AAD)' -* Click on '__App registrations__' -* Click on '__New registration__' -* 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__' -* Click on '__New client secret__' -* 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 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 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. 'AzureGovernanceVisualizer_SC'). -* Select '__Next__', '__Review + Assign__' - -#### Azure DevOps -* Click on '__Project settings__' (located on the bottom left) -* Under '__Pipelines__' click on '__Service Connections__' -* Click on '__New service connection__' and select the connection/service type '__Azure Resource Manager__' and click '__Next__' -* For the authentication method select '__Service principal (manual)__' and click '__Next__' -* For the '__Scope level__' select '__Management Group__' - * In the field '__Management Group Id__' enter the target Management Group Id - * In the field '__Management Group Name__' enter the target Management Group Name -* Under '__Authentication__' in the field '__Service Principal Id__' enter the '__Application (client) ID__' that you copied away earlier -* For the '__Credential__' select '__Service principal key__', in the field '__Service principal key__' enter the secret that you copied away earlier -* For '__Tenant ID__' enter your Tenant Id -* Click on '__Verify__' -* Under '__Details__' provide your Service Connection with a name and copy away the name as we will need that later when editing the Pipeline YAML file -* For '__Security__' leave the 'Grant access permissions to all pipelines' option checked (optional) -* Click on '__Verify and save__' - -### Create AzDO Service Connection - Option 2 - Create Service Connection in AzDO - -* Click on '__Project settings__' (located on the bottom left) -* Under '__Pipelines__' click on '__Service connections__' -* Click on '__New service connection__' and select the connection/service type '__Azure Resource Manager__' and click '__Next__' -* For the authentication method select '__Service principal (automatic)__' and click '__Next__' -* For the '__Scope level__' select '__Management Group__', in the Management Group dropdown select the target Management Group (here the Management Group´s display names will be shown), in the '__Details__' section apply a Service Connection name and optional give it a description and click '__Save__' -* A new window will open, authenticate with your administrative account -* Now the Service Connection has been created - -__Important!__ In Azure on the target Management Group scope an '__Owner__' RBAC Role assignment for the Service Connection´s Service Principal has been created automatically (we do however only require a '__Reader__' RBAC Role assignment! we will take corrective action in the next steps) - -## 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 AzDO Service Connection´s Service Principal - -* PowerShell - -```powershell -$objectId = "" -$role = "Reader" -$managementGroupId = "" - -New-AzRoleAssignment ` --ObjectId $objectId ` --RoleDefinitionName $role ` --Scope /providers/Microsoft.Management/managementGroups/$managementGroupId -``` - -* Azure Portal -[Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) - -__Important!__ If you have created the AzDO Service Connection in AzDO (Option 2) then you SHOULD remove the automatically created '__Owner__' RBAC Role assignment for the AzDO Service Connection´s Service Principal from the target Management Group - -## Grant permissions in Microsoft Entra ID - -### API permissions - -* Requirements - * To grant API permissions and grant admin consent for the directory, 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)) - -Grant API permissions for the Service Principal´s Application that we created earlier - -* Navigate to 'Microsoft Entra ID (AAD)' -* Click on '__App registrations__' -* Search for the Application that we created earlier and click on it -* Under '__Manage__' click on '__API permissions__' - * Click on '__Add a permissions__' - * Click on '__Microsoft Graph__' - * Click on '__Application permissions__' - * Select the following set of permissions and click '__Add permissions__' - * __Application / Application.Read.All__ - * __Group / Group.Read.All__ - * __User / User.Read.All__ - * __PrivilegedAccess / PrivilegedAccess.Read.AzureResources__ - * Click on 'Add a permissions' - * Back in the main '__API permissions__' menu you will find the permissions with status 'Not granted for...'. Click on '__Grant admin consent for _TenantName___' and confirm by click on '__Yes__' - * Now you will find the permissions with status '__Granted for _TenantName___' - -Permissions in Microsoft Entra ID (AAD) for App registration: -![alt text](img/aadpermissionsportal_4.jpg "Permissions in Microsoft Entra ID (AAD)") - -## Grant permissions on Azure Governance Visualizer AzDO 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 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 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 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) - * Enter the Management Group Id (ManagementGroupId) -* Click '__Commit__' - -## OPTION 1 (legacy) - Create AzDO Pipeline (.pipelines folder) - -* Click on '__Pipelines__' -* Click on '__New pipeline__' -* Select '__Azure Repos Git__' -* 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__' - -## OPTION 2 (new) - Edit AzDO Variables YAML file (.azuredevops folder) - ->For the '__parameters__' and '__variables__' sections, details about each parameter or variable is documented inline. - -* Click on '__Repos__' -* 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) - * [Optional] Update the '__ExcludedResourceTypesDiagnosticsCapableParameters__' - * [Optional] Update the '__SubscriptionQuotaIdWhitelistParameters__' -* Update the '__Required Variables__' section: - * Replace `` with the Service connection name you copied earlier (ServiceConnection) - * Replace `` with the Management Group Id (ManagementGroupId) -* If needed, update the '__Default Variables__' section -* If needed, update the '__Optional Variables__' section - -### OPTION 2 (new) Create AzDO Pipeline (.azuredevops folder) - -* Click on '__Pipelines__' -* Click on '__New pipeline__' -* Select '__Azure Repos Git__' -* Select the Azure Governance Visualizer repository -* Click on '__Existing Azure Pipelines YAML file__' -* Under '__Path__' select '__/.azuredevops/pipelines/AzGovViz.pipeline.yml__' -* Click ' __Save__' - -## Run the AzDO Pipeline - -* Click on '__Pipelines__' -* Select the Azure Governance Visualizer pipeline -* Click '__Run pipeline__' - -Note: Before the pipeline kicks off it may require you to approve the run (only first time run) - -## Create AzDO Wiki (WikiAsCode) - -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 Azure Governance Visualizer repository -* Select the folder '__wiki__' and click '__OK__' -* Enter a name for the Wiki -* Click '__Publish__' - -# Azure Governance Visualizer in GitHub Actions - -## Create GitHub repository - -Create a 'private' repository - -## Import Code - -Click on 'Import code' - -Use 'https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git' as clone URL - -Click on 'Begin import' - -Navigate to your newly created repository -In the folder `./github/workflows` two worklows are available: -1. [AzGovViz.yml](#azgovviz-yaml) -Use this workflow if you want to store your Application (App registration) secret in GitHub - -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 - -## 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 'Microsoft Entra ID (AAD)' -* Click on '__App registrations__' -* Click on '__New registration__' -* 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__' -* Click on '__New client secret__' -* 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 (Azure Governance Visualizer YAML) - -In GitHub navigate to 'Settings' -* Click on 'Secrets' -* Click on 'Actions' -* Click 'New repository secret' - * Name: CREDS - * Value: -``` -{ - "tenantId": "", - "subscriptionId": "", - "clientId": "", - "clientSecret": "" -} -``` - -### Workflow permissions - -In GitHub navigate to 'Settings' -* Click on 'Actions' -* Click on 'General' -* Under 'Workflow permissions' select '__Read and write permissions__' -* Click 'Save' - -### 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 Azure Governance Visualizer then enable the `schedule` in the `on` section - -### 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 Azure Governance Visualizer workflow -* Click 'Run workflow' - -## 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 not have the requirement to create a secret, nore store it in GitHub - awesome :) - -* Navigate to 'Microsoft Entra ID (AAD)' -* Click on '__App registrations__' -* Click on '__New registration__' -* 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__' -* Click on '__Federated credentials__' -* Click 'Add credential' -* Select Federation credential scenario 'GitHub Actions deploying Azure Resources' -* 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 Azure Governance Visualizer repository) -* Fill the field 'Name' with a name (e.g. AzureGovernanceVisualizer_GitHub_Actions) -* Click 'Add' - -### Store the credentials in GitHub (Azure Governance Visualizer OIDC YAML) - -In GitHub navigate to 'Settings' -* Click on 'Secrets' -* Click on 'Actions' -* Click 'New repository secret' -* Create the following three secrets: - * Name: CLIENT_ID - Value: `Application (client) ID` - * Name: TENANT_ID - Value: `Tenant ID` - * Name: SUBSCRIPTION_ID - Value: `Subscription ID` - -### Workflow permissions - -In GitHub navigate to 'Settings' -* Click on 'Actions' -* Click on 'General' -* Under 'Workflow permissions' select '__Read and write permissions__' -* Click 'Save' - -### 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 Azure Governance Visualizer then enable the `schedule` in the `on` section - -### 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' - -# 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 "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces1.png "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces2.png "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces3.png "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces4.png "Azure Governance Visualizer GitHub Codespaces") - -# 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. - -There are a few models to do this, the option below is one way to get you started. - -## Prerequisites -* Deploy a simple webapp on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform -![alt text](img/webapp_create.png "Web App Create") -* Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on -![alt text](img/webapp_configure.png "Web App Configure") -* No need to configure anything, unless your organization policies require you to do so -NOTE: it is a good practice to tag your resource for operational and finance reasons -* In the webapp _Configuration_ add the name of the HTML output file to the _Default Documents_ -![alt text](img/webapp_defaultdocs.png "Web App Default documents") -* Make sure to configure Authentication! -![alt text](img/webapp_authentication.png "Web App Authentication") - -## Azure DevOps - -* Assign the Azure DevOps Service Connection´s Service Principal with RBAC Role __Website Contributor__ on the Azure Web App -* Edit the `.azuredevops/AzGovViz.variables.yml` file -![alt text](img/webapp_AzDO_yml.png "Azure DevOps YAML variables") - -## GitHub Actions - -* Assign the Service Principal used in GitHub with RBAC Role __Website Contributor__ on the Azure Web App -* Edit the `.github/workflows/AzGovViz_OIDC.yml` or `.github/workflows/AzGovViz.yml` file -![alt text](img/webapp_GitHub_yml.png "GitHub YAML variables") +This benefits from contributors. To learn how to contribute see our [Contribution guide](./contributionGuide.md). From e9dfb2cf9fbb2aec707e9402362ba307bb7ad61b Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Mon, 8 Jan 2024 23:33:30 +0000 Subject: [PATCH 03/18] couple small tweaks --- run-from/console.md | 93 +++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/run-from/console.md b/run-from/console.md index 936d79da..78583f14 100644 --- a/run-from/console.md +++ b/run-from/console.md @@ -5,31 +5,6 @@ When trying out Azure Governance Visualizer for the first time or simply as a on Some steps have both **portal based** ( :computer_mouse: ) and **PowerShell based** ( :keyboard: ) instructions. Use whichever you feel is appropriate for your situation, they both will produce the same results. -The identity executing this script will need read access on the target management group and some basic Microsoft Entra ID permissions. Follow the instructions below based on the type of user you're executing this as. - ----- - -- Requirements - -Create a '**Reader**' RBAC Role assignment on the target Management Group scope for the identity that shall run Azure Governance Visualizer - -- PowerShell - -```powershell -$objectId = "" -$managementGroupId = "" - -New-AzRoleAssignment ` --ObjectId $objectId ` --RoleDefinitionName "Reader" ` --Scope /providers/Microsoft.Management/managementGroups/$managementGroupId -``` - -- Azure Portal -[Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) - ---- - ## Prerequisites The following must be installed on the workstation that will be used to run the scripts: @@ -39,13 +14,13 @@ The following must be installed on the workstation that will be used to run the - [Azure PowerShell](https://learn.microsoft.com/powershell/azure/install-azure-powershell) - [AzAPICall](https://github.com/JulianHayward/AzAPICall#get--set-azapicall-powershell-module) -## 1. Validate permissions on user +## 1. Validate Microsoft Graph permissions for your user -:arrow_forward: If your user is a tenant _member user_ and you use your user to complete these instructions, then no additional setup is necessary. This is the most common. And you can :arrow_down_small: continue with [**2. Clone the Azure Governance Visualizer repository**](#2-clone-the-azure-governance-visualizer-repository). +:arrow_forward: If your user is a tenant _member user_ and you plan on running the script as yourself, then no additional setup is necessary. This is the most common. You can :arrow_down_small: continue with [**2. Validate Azure permissions for your user**](#2-validate-azure-permissions-for-your-user). _- or -_ -:arrow_forward: However, if your user is tenant _guest user_ and you use your user to complete these instructions, continue to [Set up to execute as a tenant _guest user_](#set-up-to-execute-as-a-tenant-guest-user) to ensure your user is configured properly. +:arrow_forward: However, if your user is tenant _guest user_ and you plan on running the script as yourself, continue to [Set up to execute as a tenant _guest user_](#set-up-to-execute-as-a-tenant-guest-user) to ensure your user is configured properly. You will likely need support from the Microsoft Entra ID administrator of the tenant you are a guest in. _- or -_ @@ -53,28 +28,24 @@ _- or -_ ### Set up to execute as a tenant _guest user_ -Your user is a guest user in the tenant or there are other hardened restrictions on the tenant, then your user must first be assigned the Microsoft Entra ID role '**Directory readers**'. +Your user is a [guest user](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#compare-member-and-guest-default-permissions) in the tenant or there are other [hardened restrictions](https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions) on the tenant, then your user must first be assigned the Microsoft Entra ID role '**Directory readers**'. Work with the Microsoft Entra administrator for the tenant you are a guest in to have them assign the '**Directory readers**' [role to your guest account](https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal). -:bulb: [Compare member and guest default permissions](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#compare-member-and-guest-default-permissions) - -:bulb: [Restrict guest access permissions in Microsoft Entra ID](https://docs.microsoft.com/azure/active-directory/enterprise-users/users-restrict-guest-permissions) - -Work with your Microsoft Entra '**Privileged Role Administrator**' or '**Global Administrator**' to assign the '**Directory readers**' [role to your guest account](https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal). - -:arrow_down_small: Continue with [**2. Clone the Azure Governance Visualizer repository**](#2-clone-the-azure-governance-visualizer-repository). +:arrow_down_small: Once that is configured, continue with [**2. Validate Azure permissions for your user**](#2-validate-azure-permissions-for-your-user). ### Set up to execute as a _service principal_ -You are planning on executing the script as a service principal instead of as your user. A service principal, by default, has no read permissions on users, groups, and other service principals, therefore you'll need to work with a Microsoft Entra ID administrator to grant additional permissions to the service principal. The following Microsoft Graph API permissions, with admin consent, need to be added: +You are planning on executing the script as a service principal instead of as your user. A service principal, by default, has no read permissions on users, groups, and other service principals, therefore you'll need to work with a Microsoft Entra ID administrator to grant additional permissions to the service principal. The following Microsoft Graph API permissions, with admin consent, need to be granted: -- **Application / Application.Read.All** -- **Group / Group.Read.All** -- **User / User.Read.All** -- **PrivilegedAccess / PrivilegedAccess.Read.AzureResources** +- '**Application / Application.Read.All**' +- '**Group / Group.Read.All**' +- '**User / User.Read.All**' +- '**PrivilegedAccess / PrivilegedAccess.Read.AzureResources**' + +#### Assign Microsoft Graph permissions, if needed **:computer_mouse: Use the Microsoft Entra admin center to assign permissions to the service principal:** -To grant API permissions and grant admin consent for the directory, the user performing the following steps must have '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. +> To grant API permissions and admin consent for the directory, the user performing the following steps must have '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/). 1. Click on '**App registrations**' @@ -96,14 +67,38 @@ Permissions and admin consent granted in Microsoft Entra ID for the service prin ![Permissions in Microsoft Entra ID](../img/aadpermissionsportal_4.jpg) -## 2. Clone the Azure Governance Visualizer repository +## 2. Validate Azure permissions for your user + +The identity executing the script (your user or the service principal) needs to have the '**Reader**' Azure RBAC role assignment on the **target management group**. + +### Assign Azure permissions, if needed + +If that permission is not yet assigned to your user or the service principal, a user with '**Microsoft.Authorization/roleAssignments/write**' permissions on the target management group scope (such as the built-in Azure RBAC role '**User Access Administrator**' or '**Owner**') is required to make the required permission changes. + +**:computer_mouse: Use the Azure portal to validate and assign the role:** + +Follow the instructions at [Assign Azure roles using the Azure portal](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-portal) to grant Azure RBAC '**Reader**' role to the management group. + +**:keyboard: Use PowerShell to assign the role:** + +```powershell +$objectId = "" +$managementGroupId = "" + +New-AzRoleAssignment ` +-ObjectId $objectId ` +-RoleDefinitionName "Reader" ` +-Scope /providers/Microsoft.Management/managementGroups/$managementGroupId +``` + +## 3. Clone the Azure Governance Visualizer repository ```powershell Set-Location "c:\Git" git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" ``` -## 2. Authenticate to Azure +## 4. Authenticate to Azure **As your user:** @@ -111,7 +106,9 @@ git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.gi Connect-AzAccount -TenantId -UseDeviceAuthentication ``` -**As the configured service principal:** +_- or -_ + +**As the service principal:** Have the '**Application (client) ID**' of the app registration OR '**Application ID**' of the service principal (Enterprise application) and the secret of the app registration at hand. @@ -124,7 +121,7 @@ User: Enter '**Application (client) ID**' of the App registration OR '**Applicat Password for user \: Enter App registration's secret -## 3. Run the Azure Governance Visualizer +## 5. Run the Azure Governance Visualizer Familiarize yourself with the available [parameters](../README.md#parameters) for Azure Governance Visualizer. @@ -137,3 +134,7 @@ If not using the `-OutputPath` parameter, all output will be created in the curr ```powershell c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -OutputPath "c:\AzGovViz-Output" ``` + +## 6. View the results + +TODO From f9af6d1038a1616ba1f59c564ada67b90b2b481f Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Tue, 9 Jan 2024 16:05:44 +0000 Subject: [PATCH 04/18] try it out, and next steps --- run-from/console.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/run-from/console.md b/run-from/console.md index 78583f14..298cc9cf 100644 --- a/run-from/console.md +++ b/run-from/console.md @@ -93,9 +93,11 @@ New-AzRoleAssignment ` ## 3. Clone the Azure Governance Visualizer repository +You'll need a copy of this repository on your workstation. + ```powershell -Set-Location "c:\Git" git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" +Set-Location "Azure-MG-Sub-Governance-Reporting" ``` ## 4. Authenticate to Azure @@ -123,18 +125,24 @@ Password for user \: Enter App registration's secret ## 5. Run the Azure Governance Visualizer -Familiarize yourself with the available [parameters](../README.md#parameters) for Azure Governance Visualizer. +Familiarize yourself with the available [parameters](../README.md#parameters) for Azure Governance Visualizer. The following example will create the output in directory **c:\AzGovViz-Output** (directory must exist) ```powershell -c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId +.\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -OutputPath "c:\AzGovViz-Output" ``` -If not using the `-OutputPath` parameter, all output will be created in the current directory. The following example will create the output in directory c:\AzGovViz-Output (directory must exist) +## 6. View the results + +Open the generated HTML in your default browser. ```powershell -c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -OutputPath "c:\AzGovViz-Output" +Set-Location -Path "c:\AzGovViz-Output" +Get-ChildItem +Invoke-Item ".\AzGovViz*.html" ``` -## 6. View the results +There is also a markdown version available as well in the output directory. + +## Next steps -TODO +Consider a solution that automates the execution of this process to have regular snapshots of this data available for review. This repo has instructions available to automate using [Azure DevOps](azure-devops.md) or [GitHub](github.md). For report hosting, consider using the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) which will give you an example on how to host the output on Azure Web Apps in conjunction with the automation. From 55b592c732ba023533e0144201965a436636bb83 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Tue, 9 Jan 2024 18:36:46 +0000 Subject: [PATCH 05/18] Azure DevOps version --- run-from/azure-devops.md | 353 ++++++++++++++++----------------------- 1 file changed, 148 insertions(+), 205 deletions(-) diff --git a/run-from/azure-devops.md b/run-from/azure-devops.md index 4aacee12..5f218ebf 100644 --- a/run-from/azure-devops.md +++ b/run-from/azure-devops.md @@ -1,272 +1,215 @@ # Configure and run Azure Governance Visualizer from Azure DevOps -Also, most steps have both **portal based** ( :computer_mouse: ) and **PowerShell based** ( :keyboard: ) instructions. Use whichever you feel is appropriate for your situation, they both will produce the same results. +Azure DevOps can be used to orchestrate regular execution of Azure Governance Visualizer against your target management group. This allows headless, automated execution along with the ability to set least privileges on the executing account. It uses Azure pipelines as the workflow orchestrator. These instructions will get you up and running from Azure DevOps. -## Create AzDO Project +## Prerequisites -[Create a project](https://docs.microsoft.com/en-us/azure/devops/organizations/projects/create-project?view=azure-devops&tabs=preview-page#create-a-project) +- An Azure DevOps account in which you have enough permissions to create a new project. +- Your user must have '**Microsoft.Authorization/roleAssignments/write**' permissions on the target management group scope (such as the built-in Azure RBAC role '**User Access Administrator**' or '**Owner**'). This is required to make the required permission changes. If you cannot do this yourself, follow these instructions along with someone who can. +- To grant Microsoft Graph API permissions and grant admin consent for the Microsoft Entra directory, you must yourself or work with someone that has '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. (See [Assign Microsoft Entra roles to users](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal).) -## Import Azure Governance Visualizer GitHub repository +## 1. Create an Azure DevOps project -Azure Governance Visualizer Clone URL: `https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git` +[Create an Azure DevOps project](https://learn.microsoft.com/azure/devops/organizations/projects/create-project?view=azure-devops&tabs=browser#create-a-project) to use as the home for the pipelines, artifacts, and service connections. -[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) +While you could use an existing one, these instructions are written with a new project in mind. Adjust as needed to use an existing project. -Note: the Azure Governance Visualizer GitHub repository is public - no authorization required +## 2. Import Azure Governance Visualizer GitHub repository -## Create AzDO Service Connection +Azure Governance Visualizer clone URL: `https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git` -For the pipeline to authenticate and connect to Azure we need to create an AzDO Service Connection which basically is a Service Principal (Application) -There are two options to create the Service Connection: +Follow the instructions on Microsoft Learn to [Import this repo into a new repo](https://learn.microsoft.com/azure/devops/repos/git/import-git-repository?view=azure-devops#import-into-a-new-repo). The Azure Governance Visualizer GitHub repository is public, so no authorization is required. -* Options - * **Option 1** Create Service Connection´s Service Principal in the Azure Portal - * **Option 2** Create Service Connection in AzDO +## 3. Create Azure DevOps Pipeline service connection -### Create AzDO Service Connection - Option 1 - Create Service Connections Service Principal in the Azure Portal +For pipelines to authenticate and connect to Azure you need to create an Azure Resource Manager service connection. This will allow the Azure Governance Visualizer scripts to connect to Azure resources under the identity of a properly permissioned service principal. -#### AzDO supports Open ID Connect - OIDC +There are a few options to create the service connection, both will result in least privilege access: -Using OIDC we will not have the requirement to create a secret, nore store it in AzDO - awesome :) +- **Option 1** - [Use workload identity federation](#option-1---use-workload-identity-federation-recommended) _(This is the recommended option.)_ +- **Option 2** - [Create a service principal service connection from Azure DevOps](#option-2---create-a-service-principal-service-connection-from-azure-devops) -Quick guide for an app registration: +### Option 1 - Use workload identity federation (recommended) -**AzDO:** +This option uses Microsoft Entra workload identity federation to manage a service principal but without also managing secrets or secret expiration. This is the recommended method. -* Click on '**Project settings**' (located on the bottom left) -* Under '**Pipelines**' click on '**Service Connections**' -* Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' -* Select Authentication method **Workload Identity federation (manual)** +#### Start the process in Azure DevOps -![alt text](img/azdo_oidc_0.jpg "Microsoft Entra ID (AAD) Federated credentials") +1. Open your project in [Azure DevOps](http://dev.azure.com/). +1. Click on '**Project settings**' (located on the bottom left) +1. Under '**Pipelines**' click on '**Service Connections**' +1. Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' +1. Select Authentication method **Workload Identity federation (manual)** -Copy away: + ![Microsoft Entra ID Federated credentials](../img/azdo_oidc_0.jpg) -* value of **Issuer** -* value of **Subject identifier** +1. Give the connection a meaningful name and description. +1. For '**Security**' leave the 'Grant access permissions to all pipelines' option checked (optional) +1. Copy the values of the following results, as you'll need those in the next set of steps for Microsoft Entra configuration. -![alt text](img/azdo_oidc_1.jpg "Microsoft Entra ID (AAD) Federated credentials; issuer, subject identifier") + - **Issuer** + - **Subject identifier** -**Microsoft Entra ID (AAD):** + ![Microsoft Entra ID federated credentials; issuer, subject identifier](../img/azdo_oidc_1.jpg) -* In the Azure Portal navigate to 'Microsoft Entra ID (AAD)' -* Click on '**App registrations**' -* Click on '**New registration**' -* Name your application (e.g. 'AzureGovernanceVisualizer_SC') -* Click '**Register**' -* Your App registration has been created -* Under '**Manage**' click on '**Certificates & Secrets**' -* Click on '**Federated credentials**' and '**Add credential**' +#### Continue by creating and configuring an app registration in Microsoft Entra ID -![alt text](img/azdo_aad_oidc_0.jpg "Microsoft Entra ID (AAD) Federated credentials") +Doing the "manual" path requires jumping over to Microsoft Entra ID to create a service principal before continuing creating the Azure service connection. -Paste the just copied off +1. In a new browser tab, navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/) +1. Click on '**App registrations**' +1. Click on '**New registration**' +1. Name your application (e.g. 'AzureGovernanceVisualizer_SC') +1. Click '**Register**' +1. Your App registration has been created +1. Copy (note) the '**Application (client) ID**', as you'll need it back in Azure DevOps. +1. Under '**Manage**' click on '**Certificates & Secrets**' +1. Click on '**Federated credentials**' and '**Add credential**' + ![Microsoft Entra ID federated credentials](../img/azdo_aad_oidc_0.jpg) +1. Paste the values copied in a prior step. + - **Issuer** + - **Subject identifier** -* value for **Issuer** -* value for **Subject identifier** + ![Microsoft Entra ID (AAD) Federated credentials; issuer, subject identifier](../img/azdo_aad_oidc_1.jpg) -![alt text](img/azdo_aad_oidc_1.jpg "Microsoft Entra ID (AAD) Federated credentials; issuer, subject identifier") +#### Assign least privileges to the new identity from the Azure portal -#### Azure Portal +1. In the [Azure portal](https://portal.azure.com) proceed to '**Management Groups**', select the scope at which Azure Governance Visualizer will run. This is usually the **Tenant Root Group**. +1. Go to '**Access Control (IAM)**', '**Grant Access**' and '**Add Role Assignment**', select '**Reader**', click '**Next**' +1. Now '**Select Member**', this will be the name of the Application you created above (e.g. 'AzureGovernanceVisualizer_SC'). +1. Select '**Next**', '**Review + Assign**' -* Navigate to 'Microsoft Entra ID (AAD)' -* Click on '**App registrations**' -* Click on '**New registration**' -* 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**' -* Click on '**New client secret**' -* 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 Service Connection in AzDO +#### Complete the process in Azure DevOps -**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. +1. Return to the tab with the "New Azure service connection" workflow in [Azure DevOps](http://dev.azure.com/) and pick up where you left off. +1. For the '**Scope level**' select '**Management Group**' + - In the field '**Management Group Id**' enter the target Azure management group ID + - In the field '**Management Group Name**' enter the target Azure management group name +1. Under '**Authentication**' in the field '**Service Principal Id**' enter the '**Application (client) ID**' that you noted earlier. +1. For '**Tenant ID**' enter your Microsoft Entra tenant ID. +1. Click on '**Verify and save**' -* 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. 'AzureGovernanceVisualizer_SC'). -* Select '**Next**', '**Review + Assign**' +### Option 2 - Create a service principal service connection from Azure DevOps -#### Azure DevOps +This option uses an automatically created service principal, but requires some authentication tweaks to achieve least privilege. The service principal secret that automatically used in this option will expire in three months, after which you need to refresh the service connection. -* Click on '**Project settings**' (located on the bottom left) -* Under '**Pipelines**' click on '**Service Connections**' -* Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' -* For the authentication method select '**Service principal (manual)**' and click '**Next**' -* For the '**Scope level**' select '**Management Group**' - * In the field '**Management Group Id**' enter the target Management Group Id - * In the field '**Management Group Name**' enter the target Management Group Name -* Under '**Authentication**' in the field '**Service Principal Id**' enter the '**Application (client) ID**' that you copied away earlier -* For the '**Credential**' select '**Service principal key**', in the field '**Service principal key**' enter the secret that you copied away earlier -* For '**Tenant ID**' enter your Tenant Id -* Click on '**Verify**' -* Under '**Details**' provide your Service Connection with a name and copy away the name as we will need that later when editing the Pipeline YAML file -* For '**Security**' leave the 'Grant access permissions to all pipelines' option checked (optional) -* Click on '**Verify and save**' +1. Open your project in [Azure DevOps](http://dev.azure.com/). +1. Click on '**Project settings**' (located on the bottom left) +1. Under '**Pipelines**' click on '**Service connections**' +1. Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' +1. For the authentication method select '**Service principal (automatic)**' and click '**Next**' +1. For the '**Scope level**' select '**Management Group**', in the Management Group dropdown select the target Management Group (here the Management Group's display names will be shown), in the '**Details**' section apply a Service Connection name and optional give it a description +1. Click '**Save**' +1. A new window will open, authenticate with your administrative account -### Create AzDO Service Connection - Option 2 - Create Service Connection in AzDO +**Important!** In Azure, on the target management group scope an '**Owner**' Azure RBAC role assignment for the service connection's service principal has been created automatically. This is more permissions than necessary, as the Azure Governance Visualizer only requires '**Reader**' role assignment. This will be corrected in the next steps. -* Click on '**Project settings**' (located on the bottom left) -* Under '**Pipelines**' click on '**Service connections**' -* Click on '**New service connection**' and select the connection/service type '**Azure Resource Manager**' and click '**Next**' -* For the authentication method select '**Service principal (automatic)**' and click '**Next**' -* For the '**Scope level**' select '**Management Group**', in the Management Group dropdown select the target Management Group (here the Management Group´s display names will be shown), in the '**Details**' section apply a Service Connection name and optional give it a description and click '**Save**' -* A new window will open, authenticate with your administrative account -* Now the Service Connection has been created +#### Adjust permissions on the service principal from Azure -**Important!** In Azure on the target Management Group scope an '**Owner**' RBAC Role assignment for the Service Connection´s Service Principal has been created automatically (we do however only require a '**Reader**' RBAC Role assignment! we will take corrective action in the next steps) - -## 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 AzDO Service Connection´s Service Principal - -* PowerShell +Create a '**Reader**' Azure RBAC role assignment on the target management group scope for the AzDO service connection's service principal. ```powershell -$objectId = "" -$role = "Reader" +$objectId = "" $managementGroupId = "" New-AzRoleAssignment ` -ObjectId $objectId ` --RoleDefinitionName $role ` +-RoleDefinitionName "Reader" ` -Scope /providers/Microsoft.Management/managementGroups/$managementGroupId -``` -* Azure Portal -[Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) +Remove-AzRoleAssignment ` +-ObjectId $objectId ` +-RoleDefinitionName "Owner" ` +-Scope /providers/Microsoft.Management/managementGroups/$managementGroupId +``` -**Important!** If you have created the AzDO Service Connection in AzDO (Option 2) then you SHOULD remove the automatically created '**Owner**' RBAC Role assignment for the AzDO Service Connection´s Service Principal from the target Management Group +If you'd like to use the Azure portal instead; follow the instructions on Microsoft Learn to [Assign Azure roles using the Azure portal](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-portal). Grant the service principal the '**Reader**' role on the target management group. Then follow the instructions to [Remove Azure role assignments](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-remove) to remove the automatically assigned '**Owner**' role from the service principal on the target management group. -## Grant permissions in Microsoft Entra ID - -### API permissions - -* Requirements - * To grant API permissions and grant admin consent for the directory, 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)) +## 4. Grant Microsoft Graph API permissions in Microsoft Entra ID -Grant API permissions for the Service Principal´s Application that we created earlier +The service principal created in the prior step is authorized for Azure resource access, but also must be authorized for Microsoft Entra ID directory querying through Microsoft Graph. The instructions in this step will configure that. -* Navigate to 'Microsoft Entra ID (AAD)' -* Click on '**App registrations**' -* Search for the Application that we created earlier and click on it -* Under '**Manage**' click on '**API permissions**' - * Click on '**Add a permissions**' - * Click on '**Microsoft Graph**' - * Click on '**Application permissions**' - * Select the following set of permissions and click '**Add permissions**' - * **Application / Application.Read.All** - * **Group / Group.Read.All** - * **User / User.Read.All** - * **PrivilegedAccess / PrivilegedAccess.Read.AzureResources** - * Click on 'Add a permissions' - * Back in the main '**API permissions**' menu you will find the permissions with status 'Not granted for...'. Click on '**Grant admin consent for _TenantName_**' and confirm by click on '**Yes**' - * Now you will find the permissions with status '**Granted for _TenantName_**' +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/). +1. Click on '**App registrations**' +1. Search for the existing application (service principal) +1. Under '**Manage**' click on '**API permissions**' +1. Click on '**Add a permissions**' +1. Click on '**Microsoft Graph**' +1. Click on '**Application permissions**' +1. Select the following set of permissions and click '**Add permissions**' + - **Application / Application.Read.All** + - **Group / Group.Read.All** + - **User / User.Read.All** + - **PrivilegedAccess / PrivilegedAccess.Read.AzureResources** +1. Click on 'Add a permissions' +1. Back in the main '**API permissions**' menu you will find permissions with status 'Not granted for...'. Click on '**Grant admin consent for _TenantName_**' and confirm by click on '**Yes**' + - Now you will find the permissions with status '**Granted for _TenantName_**' -Permissions in Microsoft Entra ID (AAD) for App registration: -![alt text](img/aadpermissionsportal_4.jpg "Permissions in Microsoft Entra ID (AAD)") +Permissions and admin consent granted in Microsoft Entra ID for the service principal (App Registration): -## Grant permissions on Azure Governance Visualizer AzDO repository +![Permissions in Microsoft Entra ID](../img/aadpermissionsportal_4.jpg) -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 +## 5. Grant permissions on Azure Governance Visualizer Azure DevOps 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 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) +When the Azure pipeline executes the Azure Governance Visualizer script the output from the script should be pushed back to the Azure Governance Visualizer Azure DevOps repository. In order to do this, you need to grant the Azure DevOps project's 'Build Service' account with '**Contribute**' permissions on the repository. -## OPTION 1 (legacy) - Edit AzDO YAML file (.pipelines folder) +1. Open your project in [Azure DevOps](http://dev.azure.com/). +1. Click on '**Project settings**' (located on the bottom left) +1. Under '**Repos**', click '**Repositories**'. +1. Click on the Azure Governance Visualizer repository and select the tab '**Security**' +1. 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) -* Click on '**Repos**' -* 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) - * Enter the Management Group Id (ManagementGroupId) -* Click '**Commit**' +## 6. Configure parameters in the Azure pipeline YAML file -## OPTION 1 (legacy) - Create AzDO Pipeline (.pipelines folder) +You'll need to modify the '**AzGovViz.variables.yml**' file to work with the service connection you created and to point to your target management group. -* Click on '**Pipelines**' -* Click on '**New pipeline**' -* Select '**Azure Repos Git**' -* 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**' +> For the '**parameters**' and '**variables**' sections, details about each parameter or variable is documented inline. -## OPTION 2 (new) - Edit AzDO Variables YAML file (.azuredevops folder) +1. Open your project in [Azure DevOps](http://dev.azure.com/). +1. Click on '**Repos**' +1. Navigate to the Azure Governance Visualizer repository +1. In the folder '**/.azuredevops/pipelines**' click on '**AzGovViz.variables.yml**' and click '**Edit**' +1. If needed, modify the '**parameters**' section: + - For more information, see [Runtime parameters](https://learn.microsoft.com/azure/devops/pipelines/process/runtime-parameters). + - (Optional) Update the '**ExcludedResourceTypesDiagnosticsCapableParameters**' + - (Optional) Update the '**SubscriptionQuotaIdWhitelistParameters**' +1. Update the '**Required Variables**' section: + - Replace `` with the name of the Azure DevOps service connection that you created and configured earlier (ServiceConnection) + - Replace `` with the target Azure management group ID (ManagementGroupId) +1. If needed, update the '**Default Variables**' section +1. If needed, update the '**Optional Variables**' section ->For the '**parameters**' and '**variables**' sections, details about each parameter or variable is documented inline. +## 7. Create the Azure pipeline -* Click on '**Repos**' -* 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) - * [Optional] Update the '**ExcludedResourceTypesDiagnosticsCapableParameters**' - * [Optional] Update the '**SubscriptionQuotaIdWhitelistParameters**' -* Update the '**Required Variables**' section: - * Replace `` with the Service connection name you copied earlier (ServiceConnection) - * Replace `` with the Management Group Id (ManagementGroupId) -* If needed, update the '**Default Variables**' section -* If needed, update the '**Optional Variables**' section +1. Open your project in [Azure DevOps](http://dev.azure.com/). +1. Click on '**Pipelines**' +1. Click on '**New pipeline**' +1. Select '**Azure Repos Git**' +1. Select the Azure Governance Visualizer repository +1. Click on '**Existing Azure Pipelines YAML file**' +1. Under '**Path**' select '**/.azuredevops/pipelines/AzGovViz.pipeline.yml**' +1. Click ' **Save**' -### OPTION 2 (new) Create AzDO Pipeline (.azuredevops folder) +## 8. Test the Azure pipeline with a manual run -* Click on '**Pipelines**' -* Click on '**New pipeline**' -* Select '**Azure Repos Git**' -* Select the Azure Governance Visualizer repository -* Click on '**Existing Azure Pipelines YAML file**' -* Under '**Path**' select '**/.azuredevops/pipelines/AzGovViz.pipeline.yml**' -* Click ' **Save**' +1. Click on '**Pipelines**' +1. Select the new Azure Governance Visualizer pipeline +1. Click '**Run pipeline**' -## Run the AzDO Pipeline +> Before the pipeline kicks off it may require you to approve the run. (only first time run) -* Click on '**Pipelines**' -* Select the Azure Governance Visualizer pipeline -* Click '**Run pipeline**' +## 9. Establish an Azure DevOps Wiki (WikiAsCode) -Note: Before the pipeline kicks off it may require you to approve the run (only first time run) +Once the pipeline has executed successfully you can setup your Wiki -## Create AzDO Wiki (WikiAsCode) +1. Click on '**Overview**' +1. Click on '**Wiki**' +1. Click on '**Publish code as wiki**' +1. Select the Azure Governance Visualizer repository +1. Select the folder '**wiki**' and click '**OK**' +1. Enter a name for the Wiki +1. Click '**Publish**' -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 Azure Governance Visualizer repository -* Select the folder '**wiki**' and click '**OK**' -* Enter a name for the Wiki -* Click '**Publish**' - -## 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. - -There are a few models to do this, the option below is one way to get you started. - -### Prerequisites - -* Deploy a simple webapp on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform -![alt text](img/webapp_create.png "Web App Create") -* Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on -![alt text](img/webapp_configure.png "Web App Configure") -* No need to configure anything, unless your organization policies require you to do so -NOTE: it is a good practice to tag your resource for operational and finance reasons -* In the webapp _Configuration_ add the name of the HTML output file to the _Default Documents_ -![alt text](img/webapp_defaultdocs.png "Web App Default documents") -* Make sure to configure Authentication! -![alt text](img/webapp_authentication.png "Web App Authentication") +## 10. Publish the Azure Governance Visualizer HTML to a Azure Web App _(Optional)_ -### Configure - -* Assign the Azure DevOps Service Connection´s Service Principal with RBAC Role **Website Contributor** on the Azure Web App -* Edit the `.azuredevops/AzGovViz.variables.yml` file -![alt text](img/webapp_AzDO_yml.png "Azure DevOps YAML variables") +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. The instructions for this can be found in the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator?tab=readme-ov-file#5-create-a-microsoft-entra-application-for-user-authentication-to-the-azure-web-app-that-will-host-azgovviz) repo. From 7e7b6442889a7a4a7724577b0a8537a44d083778 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Tue, 9 Jan 2024 19:41:27 +0000 Subject: [PATCH 06/18] GitHub instructions --- run-from/azure-devops.md | 8 +- run-from/console.md | 20 ++- run-from/github.md | 271 ++++++++++++++++----------------------- setup.md | 29 ++--- 4 files changed, 143 insertions(+), 185 deletions(-) diff --git a/run-from/azure-devops.md b/run-from/azure-devops.md index 5f218ebf..732fef8f 100644 --- a/run-from/azure-devops.md +++ b/run-from/azure-devops.md @@ -5,8 +5,6 @@ Azure DevOps can be used to orchestrate regular execution of Azure Governance Vi ## Prerequisites - An Azure DevOps account in which you have enough permissions to create a new project. -- Your user must have '**Microsoft.Authorization/roleAssignments/write**' permissions on the target management group scope (such as the built-in Azure RBAC role '**User Access Administrator**' or '**Owner**'). This is required to make the required permission changes. If you cannot do this yourself, follow these instructions along with someone who can. -- To grant Microsoft Graph API permissions and grant admin consent for the Microsoft Entra directory, you must yourself or work with someone that has '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. (See [Assign Microsoft Entra roles to users](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal).) ## 1. Create an Azure DevOps project @@ -31,7 +29,7 @@ There are a few options to create the service connection, both will result in le ### Option 1 - Use workload identity federation (recommended) -This option uses Microsoft Entra workload identity federation to manage a service principal but without also managing secrets or secret expiration. This is the recommended method. +This option uses Microsoft Entra workload identity federation to manage a service principal but without the need for you to manage secrets or secret expiration. This is the recommended method. #### Start the process in Azure DevOps @@ -210,6 +208,6 @@ Once the pipeline has executed successfully you can setup your Wiki 1. Enter a name for the Wiki 1. Click '**Publish**' -## 10. Publish the Azure Governance Visualizer HTML to a Azure Web App _(Optional)_ +## Next steps -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. The instructions for this can be found in the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator?tab=readme-ov-file#5-create-a-microsoft-entra-application-for-user-authentication-to-the-azure-web-app-that-will-host-azgovviz) repo. +For report hosting, consider using the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) which will give you an example on how to host the output on Azure Web Apps in conjunction with this Azure DevOps automation. diff --git a/run-from/console.md b/run-from/console.md index 298cc9cf..d55c4d1d 100644 --- a/run-from/console.md +++ b/run-from/console.md @@ -11,9 +11,11 @@ The following must be installed on the workstation that will be used to run the - [Git](https://git-scm.com/downloads) - [PowerShell 7](https://github.com/PowerShell/PowerShell#get-powershell) (minimum supported version 7.0.3) -- [Azure PowerShell](https://learn.microsoft.com/powershell/azure/install-azure-powershell) +- [Azure PowerShell](https://learn.microsoft.com/powershell/azure/install-azure-powershell), specifically `Az.Accounts`. - [AzAPICall](https://github.com/JulianHayward/AzAPICall#get--set-azapicall-powershell-module) +> There is a [dev container provided in this repo](#using-github-codespaces-as-your-console) if you'd wish to use GitHub Codespaces. + ## 1. Validate Microsoft Graph permissions for your user :arrow_forward: If your user is a tenant _member user_ and you plan on running the script as yourself, then no additional setup is necessary. This is the most common. You can :arrow_down_small: continue with [**2. Validate Azure permissions for your user**](#2-validate-azure-permissions-for-your-user). @@ -143,6 +145,22 @@ Invoke-Item ".\AzGovViz*.html" There is also a markdown version available as well in the output directory. +## Using GitHub Codespaces as your console + +This repo ships with a GitHub [Codespace](https://docs.github.com/codespaces/getting-started/quickstart) dev container configuration that has all of the [Prerequisites](#prerequisites) installed. + +### Visual screenshot tour of that experience + +![Azure Governance Visualizer GitHub Codespaces](../img/codespaces0.png) + +![Azure Governance Visualizer GitHub Codespaces](../img/codespaces1.png) + +![Azure Governance Visualizer GitHub Codespaces](../img/codespaces2.png) + +![Azure Governance Visualizer GitHub Codespaces](../img/codespaces3.png) + +![Azure Governance Visualizer GitHub Codespaces](../img/codespaces4.png) + ## Next steps Consider a solution that automates the execution of this process to have regular snapshots of this data available for review. This repo has instructions available to automate using [Azure DevOps](azure-devops.md) or [GitHub](github.md). For report hosting, consider using the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) which will give you an example on how to host the output on Azure Web Apps in conjunction with the automation. diff --git a/run-from/github.md b/run-from/github.md index 950ca7bb..167e7469 100644 --- a/run-from/github.md +++ b/run-from/github.md @@ -1,181 +1,126 @@ # Configure and run Azure Governance Visualizer from GitHub -Also, most steps have both **portal based** ( :computer_mouse: ) and **PowerShell based** ( :keyboard: ) instructions. Use whichever you feel is appropriate for your situation, they both will produce the same results. +GitHub can be used to orchestrate regular execution of Azure Governance Visualizer against your target management group. This allows headless, automated execution along with the ability to set least privileges on the executing account. It uses GitHub actions as the workflow orchestrator. These instructions will get you up and running from GitHub. + +## Prerequisites + +- A GitHub organization in which you have enough permissions to create a repository. + +## 1. Create GitHub repository + +1. Go to to start the repository creation process. +1. Use '**https:\//github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git**' as the clone URL. +1. Select your existing GitHub organization. +1. Select 'Private' +1. Click on 'Begin import' +1. Navigate to your newly created repository + +If you'd instead like to perform this from the GitHub CLI, see [gh repo create](https://cli.github.com/manual/gh_repo_create) for instructions. + +## 2. Create and configure a service principal + +For GitHub actions to authenticate and connect to Azure you need to create a service principal. This will allow the Azure Governance Visualizer scripts to connect to Azure resources and Microsoft Graph with a properly permissioned identity. + +There are a few options to create the service principal, both will result in least privilege access: -## Create GitHub repository +- **Option 1** - [Use workload identity federation](#option-1---use-workload-identity-federation-recommended) _(This is the recommended option.)_ +- **Option 2** - [Create and manage a service principal](#option-2---create-and-manage-a-service-principal) -Create a 'private' repository +### Option 1 - Use workload identity federation (recommended) -## Import Code +This option uses Microsoft Entra workload identity federation to manage a service principal you create but without also the need for you to manage secrets or secret expiration. This process uses the [OIDC (OpenID Connect) feature](https://docs.github.com/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure) of GitHub workflows. This process uses the **[.github/workflows/AzGovViz_OIDC.yml](../.github/workflows/AzGovViz_OIDC.yml)** workflow file and is the recommended method. -Click on 'Import code' +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/) +1. Click on '**App registrations**' +1. Click on '**New registration**' +1. Name your application (e.g. 'AzureGovernanceVisualizer_SC') +1. Click '**Register**' +1. Your App registration has been created, in the '**Overview**' copy the '**Application (client) ID**' as we will need it later to setup the connection +1. Under '**Manage**' click on '**Certificates & Secrets**' +1. Click on '**Federated credentials**' +1. Click 'Add credential' +1. Select Federation credential scenario 'GitHub Actions deploying Azure Resources' +1. Fill the field 'Organization' with your GitHub Organization name +1. Fill the field 'Repository' with your GitHub repository name +1. For the entity type select 'Branch' +1. Fill the field 'GitHub branch name' with your branch name (default is 'master' if you imported the Azure Governance Visualizer repository) +1. Fill the field 'Name' with a name (e.g. AzureGovernanceVisualizer_GitHub_Actions) +1. Click 'Add' -Use '' as clone URL +#### Store the service principal configuration in GitHub -Click on 'Begin import' +1. In the GitHub repository, navigate to 'Settings' +1. Click on 'Secrets' +1. Click on 'Actions' +1. Click 'New repository secret' +1. Create the following three secrets: + - Name: **CLIENT_ID** + Value: `Application (client) ID (GUID)` + - Name: **TENANT_ID** + Value: `Tenant ID (GUID)` + - Name: **SUBSCRIPTION_ID** + Value: `Subscription ID (GUID)` -Navigate to your newly created repository -In the folder `./github/workflows` two worklows are available: +### Option 2 - Create and manage a service principal -1. [AzGovViz.yml](#azgovviz-yaml) -Use this workflow if you want to store your Application (App registration) secret in GitHub +This other option has you creating a service principal and requires you to manage secrets and secret expiration for that service principal. This process uses the **[.github/workflows/AzGovViz.yml](../.github/workflows/AzGovViz.yml)** workflow file. -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 +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/) +1. Click on '**App registrations**' +1. Name your application (e.g. 'AzureGovernanceVisualizer_SC') +1. Click '**Register**' +1. 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 +1. Under '**Manage**' click on '**Certificates & Secrets**' +1. Click on '**New client secret**' +1. Provide a good description and choose the expiry time based on your need and click '**Add**' +1. A new client secret has been created, copy the secret's value as we will need it later to setup the secrets in GitHub -## Azure Governance Visualizer YAML +#### Store the newly created credentials in GitHub -For the GitHub Actiom to authenticate and connect to Azure we need to create Service Principal (Application) +1. In the GitHub repository, navigate to 'Settings' +1. Click on 'Secrets' +1. Click on 'Actions' +1. Click 'New repository secret' + - Name: **CREDS** + - Value: -In the Azure Portal navigate to 'Microsoft Entra ID (AAD)' + ```json + { + "tenantId": "", + "subscriptionId": "", + "clientId": "", + "clientSecret": "" + } + ``` -* Click on '**App registrations**' -* Click on '**New registration**' -* 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**' -* Click on '**New client secret**' -* 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 +## 3. Set GitHub workflow permissions -### Store the credentials in GitHub (Azure Governance Visualizer YAML) +1. In the GitHub repository, navigate to 'Settings' +1. Click on 'Actions' +1. Click on 'General' +1. Under 'Workflow permissions' select '**Read and write permissions**' +1. Click 'Save' -In GitHub navigate to 'Settings' - -* Click on 'Secrets' -* Click on 'Actions' -* Click 'New repository secret' - * Name: CREDS - * Value: - -``` -{ - "tenantId": "", - "subscriptionId": "", - "clientId": "", - "clientSecret": "" -} -``` - -### Workflow permissions - -In GitHub navigate to 'Settings' - -* Click on 'Actions' -* Click on 'General' -* Under 'Workflow permissions' select '**Read and write permissions**' -* Click 'Save' - -### 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 Azure Governance Visualizer then enable the `schedule` in the `on` section - -### 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 Azure Governance Visualizer workflow -* Click 'Run workflow' - -## 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 not have the requirement to create a secret, nore store it in GitHub - awesome :) - -* Navigate to 'Microsoft Entra ID (AAD)' -* Click on '**App registrations**' -* Click on '**New registration**' -* 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**' -* Click on '**Federated credentials**' -* Click 'Add credential' -* Select Federation credential scenario 'GitHub Actions deploying Azure Resources' -* 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 Azure Governance Visualizer repository) -* Fill the field 'Name' with a name (e.g. AzureGovernanceVisualizer_GitHub_Actions) -* Click 'Add' - -### Store the credentials in GitHub (Azure Governance Visualizer OIDC YAML) - -In GitHub navigate to 'Settings' - -* Click on 'Secrets' -* Click on 'Actions' -* Click 'New repository secret' -* Create the following three secrets: - * Name: CLIENT_ID - Value: `Application (client) ID` - * Name: TENANT_ID - Value: `Tenant ID` - * Name: SUBSCRIPTION_ID - Value: `Subscription ID` - -### Workflow permissions - -In GitHub navigate to 'Settings' - -* Click on 'Actions' -* Click on 'General' -* Under 'Workflow permissions' select '**Read and write permissions**' -* Click 'Save' - -### 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 Azure Governance Visualizer then enable the `schedule` in the `on` section - -### 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' - -# 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 "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces1.png "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces2.png "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces3.png "Azure Governance Visualizer GitHub Codespaces") - -![alt text](img/codespaces4.png "Azure Governance Visualizer GitHub Codespaces") - -## 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. - -There are a few models to do this, the option below is one way to get you started. - -### Prerequisites - -* Deploy a simple webapp on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform -![alt text](img/webapp_create.png "Web App Create") -* Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on -![alt text](img/webapp_configure.png "Web App Configure") -* No need to configure anything, unless your organization policies require you to do so -NOTE: it is a good practice to tag your resource for operational and finance reasons -* In the webapp _Configuration_ add the name of the HTML output file to the _Default Documents_ -![alt text](img/webapp_defaultdocs.png "Web App Default documents") -* Make sure to configure Authentication! -![alt text](img/webapp_authentication.png "Web App Authentication") - -### Configure - -* Assign the Service Principal used in GitHub with RBAC Role **Website Contributor** on the Azure Web App -* Edit the `.github/workflows/AzGovViz_OIDC.yml` or `.github/workflows/AzGovViz.yml` file -![alt text](img/webapp_GitHub_yml.png "GitHub YAML variables") +## 4. Configure the workflow YAML file + +1. In the folder `./github/workflows` edit the appropriate YAML file based on your choice in Step 2 + - **[AzGovViz_OIDC.yml](../.github/workflows/AzGovViz_OIDC.yml)** for Option 1 (workload identity federation) + - **[AzGovViz.yml](../.github/workflows/AzGovViz.yml)** for Option 2 (Normal service principal) +1. In the `env` section enter your target Azure management group ID +1. If you want to continuously run Azure Governance Visualizer then enable the `schedule` in the `on` section + +## 5. Run Azure Governance Visualizer in GitHub actions + +1. In the GitHub repository, navigate to 'Actions' +1. Click 'Enable GitHub Actions on this repository' +1. Select the configured Azure Governance Visualizer workflow file +1. Click 'Run workflow' + +## 6. Publish the Azure Governance Visualizer HTML to a Azure Web App _(Optional)_ + +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. The instructions for this can be found in the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator?tab=readme-ov-file#5-create-a-microsoft-entra-application-for-user-authentication-to-the-azure-web-app-that-will-host-azgovviz) repo. + +## Next steps + +For report hosting, consider using the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) which will give you an example on how to host the output on Azure Web Apps in conjunction with this GitHub automation. diff --git a/setup.md b/setup.md index 145f11b5..427769cc 100644 --- a/setup.md +++ b/setup.md @@ -2,34 +2,31 @@ Follow these steps to deploy the Azure Governance Visualizer. There are three sets of instructions depending on where you wish to execute it. Supported paths are: -* Running it ad-hoc from a workstation (console) -* Running it from Azure DevOps -* Running it from GitHub +- Running it ad-hoc from a workstation console or dev container +- Running it from Azure DevOps +- Running it from GitHub No matter which of the three you choose, they all evaluate the same governance concerns and produce the same reporting results, just the execution and reporting environment is distinct. Use whichever environment is best suited for your situation. -### Prerequisites +## Prerequisites -* To assign roles as part of the following instructions, 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**'). +- Your user must have '**Microsoft.Authorization/roleAssignments/write**' permissions on the target management group scope (such as the built-in Azure RBAC role '**User Access Administrator**' or '**Owner**'). This is required to make the required permission changes. If you cannot do this yourself, follow these instructions along with someone who can. +- To grant Microsoft Graph API permissions and grant admin consent for the Microsoft Entra directory, you must yourself have or work with someone that has the '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. (See [Assign Microsoft Entra roles to users](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal).) -### Set up and run Azure Governance Visualizer from the console +## Set up and run Azure Governance Visualizer from the console -To set up local execution of AzGovViz without involving Azure pipelines or GitHub actions. This solution is good for proof of value exploration, local development, etc. It's encouraged that you use Azure DevOps pipelines or GitHub actions for a formal deployment. +To set up local execution of the Azure Governance Visualizer without involving automation from Azure pipelines or GitHub actions. This solution is good for proof of value exploration, local development, etc. It's encouraged that you use Azure DevOps pipelines or GitHub actions for a formal deployment. -Follow the instructions to [Configure and run from the console](./run-from/console.md). +:arrow_right: Follow the instructions to [Configure and run from the console](./run-from/console.md). -### Set up and run Azure Governance Visualizer in Azure DevOps +## Set up and run Azure Governance Visualizer in Azure DevOps The Azure Governance Visualizer lifecycle can be hosted out of Azure DevOps. This includes automated pipelines, service connections, and even automated wiki generations. This path also optionally supports publishing the generated HTML report to Azure Web Apps. -Follow the instructions to [Configure and run from Azure DevOps](./run-from/azure-devops.md). +:arrow_right: Follow the instructions to [Configure and run from Azure DevOps](./run-from/azure-devops.md). -### Set up and run Azure Governance Visualizer in GitHub +## Set up and run Azure Governance Visualizer in GitHub To set up the Azure Governance Visualizer lifecycle, including automated actions, service connections, and GitHub Codespaces. This path also optionally supports publishing the generated HTML report to Azure Web Apps. -Follow the instructions to [Configure and run from GitHub](./run-from/github.md). - -## Contributions - -This benefits from contributors. To learn how to contribute see our [Contribution guide](./contributionGuide.md). +:arrow_right: Follow the instructions to [Configure and run from GitHub](./run-from/github.md). From 6a28dbb0c0f0f1b46d54dcc19e8c1922dd341ceb Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Tue, 9 Jan 2024 19:59:26 +0000 Subject: [PATCH 07/18] readme pass (partial) --- README.md | 143 +++++++++++++++++++++++++++--------------------------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index e1bd6f04..fa131e6c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # 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? -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. +_Do you want to get granular insights on your technical Azure Governance implementation and document it in CSV, HTML, Markdown, and JSON?_ -You can run the script either for your Tenant Root Group or any other Management Group. +Azure Governance Visualizer is a PowerShell based script that iterates your Azure tenant's management group hierarchy down to the subscription level. It captures most relevant Azure governance capabilities such as Azure Policy, RBAC, 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 Azure 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 management group or any other management group. ## Mission @@ -14,7 +15,7 @@ Challenges: * Holistic overview on governance implementation * 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__ +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) @@ -22,19 +23,18 @@ Azure Governance Visualizer is intended to help you to get a holistic overview o ### Microsoft Cloud Adoption Framework (CAF) -Listed as [tool](https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/reference/tools-templates#govern) for the Govern discipline in the Microsoft Cloud Adoption Framework - -Included in the Microsoft Cloud Adoption Framework´s [Strategy-Plan-Ready-Gov](https://azuredevopsdemogenerator.azurewebsites.net/?name=strategyplan) Azure DevOps Demo Generator template +* Listed as [tool](https://learn.microsoft.com/azure/cloud-adoption-framework/resources/tools-templates#govern) for the Govern discipline in the Microsoft Cloud Adoption Framework. +* Included in the Cloud Adoption Framework's [Strategy-Plan-Ready-Governance](https://azuredevopsdemogenerator.azurewebsites.net/?name=strategyplan) Azure DevOps Demo Generator template. ### Microsoft Well Architected Framework (WAF) -Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/architecture/framework/security/monitor-tools) in the Microsoft Well Architected Framework +* Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/architecture/framework/security/monitor-tools) in the Microsoft Well Architected Framework -### Azure Governance Visualizer Accelerator +### Azure Governance Visualizer accelerator -The [Azure Governance Visualizer Accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) provides an easy and fast deployment process that automates the creation and publishing of AzGovViz to an Azure Web Application and provides automation to configuring the pre-requisites for AzGovViz. +The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) provides an easy and fast deployment process that automates the creation and publishing of AzGovViz to an Azure Web Application and provides automation to configuring the pre-requisites for AzGovViz. -### ChatGPT +## ChatGPT ![ChatGPT](img/chatGPT.png) @@ -45,8 +45,8 @@ The [Azure Governance Visualizer Accelerator](https://github.com/Azure/Azure-Gov * [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) - * [Azure Governance Visualizer Accelerator](#azure-governance-visualizer-accelerator) - * [ChatGPT](#chatgpt) + * [Azure Governance Visualizer accelerator](#azure-governance-visualizer-accelerator) + * [ChatGPT](#chatgpt) * [Content](#content) * [Release history](#release-history) * [Demo](#demo) @@ -86,7 +86,7 @@ __Changes__ (2024-Jan-08 / 6.3.7 Minor) ## Demo -![Demo](img/demo4_66.png) +[![Demo](img/demo4_66.png)](https://www.azadvertizer.net/azgovvizv4/demo/AzGovViz_demo.html) [Demo (v6_major_20220927_1)](https://www.azadvertizer.net/azgovvizv4/demo/AzGovViz_demo.html) @@ -100,95 +100,95 @@ More [demo output](https://github.com/JulianHayward/AzGovViz) ### Slideset -Short presentation on Azure Governance Visualizer [[download](slides/AzGovViz_intro.pdf)] +Short presentation on Azure Governance Visualizer: [download](slides/AzGovViz_intro.pdf) ## Features -* __Hierarchy of Management Groups__ - * Builds a visual hierarchy of your Management Group setup including counts on linked Subscriptions, Policy assignments, scoped Policy/Set definitions and Role assignments per Management Group +* __Hierarchy of Azure management groups__ + * Builds a visual hierarchy of your management group setup including counts on linked Azure subscriptions, Azure Policy assignments, scoped policy/set definitions and role assignments per management group * __Azure Policy__ - * Custom Policy definitions + * Custom policy definitions * Scope information * Policy effect - * If Policy effect is DeployIfNotExists (DINE) will show the specified RBAC Role + * If policy effect is DeployIfNotExists (DINE) will show the specified Azure RBAC role * List of assignments * Usage in custom PolicySet definitions * System metadata 'createdOn, createdBy, updatedOn, updatedBy' ('createdBy', 'updatedBy' identity is fully resolved) - * Orphaned custom Policy definitions - * List of custom Policy definitions that matches the following criteria: + * Orphaned custom policy definitions + * List of custom policy definitions that matches the following criteria: * Policy definition is not used in any custom PolicySet definition - * No Policy assignment exists for the Policy definition + * No policy assignment exists for the policy definition * Custom PolicySet definitions * Scope information * List unique assignments - * List of Policy definitions used + * List of policy definitions used * Orphaned custom PolicySet definitions - * Criteria: no Policy assignment exists for the PolicySet definition - * Custom PolicySet definitions using deprecated built-in Policy definitions - * Policy assignments of deprecated built-in Policy definition - * Policy Exemptions - * Lists all Exemptions (scopes: Management Groups, Subscriptions, ResourceGroups, Resources) - * Enrich information on Exemption scope - * Summary on expired Exemptions + * Criteria: no policy assignment exists for the PolicySet definition + * Custom PolicySet definitions using deprecated built-in policy definitions + * Policy assignments of deprecated built-in policy definition + * Policy exemptions + * Lists all exemptions (scopes: management groups, subscriptions, resource groups, and resources) + * Enrich information on exemption scope + * Summary on expired exemptions * Policy assignments orphaned - * Policy assignments's Policy definition does not exist / likely Management Group scoped Policy defintion - Management Group deleted - * Policy assignments throughout the entirety of scopes (Management Groups, Subscriptions and Resource Groups) - * Core information on Policy assignments - * NonCompliance Message on Policy assignment for a PolicySet will only show the default non-compliance message - * Advanced/enriched information on Policy assignments + * Policy assignments's policy definition does not exist / likely management group scoped Policy definition - management group deleted + * Policy assignments throughout the entirety of scopes (management groups, subscriptions, and resource groups) + * Core information on policy assignments + * NonCompliance message on policy assignment for a PolicySet will only show the default non-compliance message + * Advanced/enriched information on policy assignments * Policy assignment scope (at scope/inheritance) - * Indicates if scope is excluded from Policy assignment - * Indicates if Exemption applies for scope - * Policy/Resource Compliance (Policy: NonCompliant, Compliant; Resource: NonCompliant, Compliant, Conflicting) - * Related RBAC Role assignments (if Policy effect is DeployIfNotExists (DINE) or Modify) - * Resolved Managed Identity (if Policy effect is DeployIfNotExists (DINE) or Modify) + * Indicates if scope is excluded from policy assignment + * Indicates if exemption applies for scope + * Policy/resource compliance (Policy: NonCompliant, Compliant; Resource: NonCompliant, Compliant, Conflicting) + * Related Azure RBAC role assignments (if policy effect is DeployIfNotExists (DINE) or Modify) + * 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 Policy Version Checker - Azure Landing Zones 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 ALZ Policy Version Checker results will be displayed in the __TenantSummary__ and a CSV export `*_ALZPolicyVersionChecker.csv` will be provided. - * Policy Remediaton - list all remediatable policies including relevant information such as assignment and definition data -* __Role-Based Access Control (RBAC)__ - * Custom Role definitions + * Azure landing zone (ALZ) policy version checker for policy and set definitions. Azure Governance Visualizer will clone the Azure landing zone GitHub repository and collect the Azure landing zone 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 policy version checker results will be displayed in the __TenantSummary__ and a CSV export `*_ALZPolicyVersionChecker.csv` will be provided. + * Policy Remediation - list all remediatable policies including relevant information such as assignment and definition data +* __Azure role-based access control (RBAC)__ + * Custom role definitions * List assignable scopes * System metadata 'createdOn, createdBy, updatedOn, updatedBy' ('createdBy', 'updatedBy' identity is fully resolved) - * Orphaned custom Role definitions - * List of custom Role definitions that matches the following criteria: - * Role definition is not used in any Role assignment - * Role is not used in a Policy definition´s rule (roleDefinitionIds) - * Orphaned Role assignments - * List of Role assignments that matches the following criteria: + * Orphaned custom role definitions + * List of custom role definitions that matches the following criteria: + * Role definition is not used in any role assignment + * Role is not used in a policy definition's rule (roleDefinitionIds) + * Orphaned role assignments + * List of role assignments that matches the following criteria: * Role definition was deleted although and assignment existed - * Role assignmet's target identity (User, Group, ServicePrincipal) was deleted - * Role assignments throughout the entirety of scopes (Management Groups, Subscriptions, Resource Groups and Resources) - * Core information on Role assignments - * Advanced information on Role assignments + * Role assignment's target identity (User, Group, ServicePrincipal) was deleted + * Role assignments throughout the entirety of scopes (management groups, subscriptions, resource groups, and resources) + * Core information on role assignments + * Advanced information on role assignments * Role assignment scope (at scope / inheritance) - * For Role Assignments on Groups the Microsoft Entra ID (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 Microsoft Entra ID (AAD) Group members count (transitive) will be reported + * For role assignments on groups the Microsoft Entra 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 Microsoft Entra 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 - * Related Policy assignments (Policy assignment that leverages the DeployIfNotExists (DINE) or Modify effect) + * Related policy assignments (Policy assignment that use the DeployIfNotExists (DINE) or Modify effect) * System metadata 'createdOn, createdBy' ('createdBy' identity is fully resolved) - * Determine if the Role assignment is 'standing' or PIM (Privileged Identity Management) managed - * Determine if the Role assignmet's Role definition is capable to write Role assignments - * PIM (Privileged Identity Management) eligibility for Role assignments - * Get a full report of all PIM eligible Role assignments for Management Groups and Subscriptions, including resolved User members of Microsoft Entra ID (AAD) Groups that have assigned eligibility - * 💡 Note: this feature requires you to execute as Service Principal with `Application` API permission `PrivilegedAccess.Read.AzureResources` + * Determine if the role assignment is 'standing' or PIM (Privileged Identity Management) managed + * Determine if the role assignment's role definition is capable to write role assignments + * PIM (Privileged Identity Management) eligibility for role assignments + * Get a full report of all PIM eligible role assignments for management groups and subscriptions, including resolved user members of Microsoft Entra ID groups that have assigned eligibility + * 💡 Note: this feature requires you to execute as service principal with `Application` API permission `PrivilegedAccess.Read.AzureResources` * Role assignments ClassicAdministrators - * Security & Best practice analysis - * Existence of custom Role definition that reflect 'Owner' permissions - * Report all Role definitions that are capable to write Role assignments, list all Role assignments for those Role definitions + * Security & best practice analysis + * Existence of custom role definition that reflect 'Owner' permissions + * Report all role definitions that are capable to write role assignments, list all role assignments for those role definitions * Role assignments for 'Owner' permissions on identity-type == 'ServicePrincipal' * Role assignments for 'Owner' permissions on identity-type != 'Group' * Role assignments for 'User Access Administrator' permissions on identity-type != 'Group' - * High priviledge Role assignments for 'Guest Users' (Owner & User Access Administrator) + * High privilege role assignments for 'Guest Users' (Owner & User Access Administrator) * __Blueprints__ * Blueprint scopes and assignments * Orphaned Blueprints -* __Management Groups__ - * Management Group count, level/depth, MG children, Sub children - * Hierarchy Settings | Default Management Group Id - * Hierarchy Settings | Require authorization for Management Group creation -* __Subscriptions, Resources & Defender__ +* __Management groups__ + * Management group count, level/depth, management group children, and sub children + * Hierarchy Settings | Default management group ID + * Hierarchy Settings | Require authorization for management group creation +* __Subscriptions, resources & Microsoft Defender__ * Subscription insights * State * QuotaId @@ -347,7 +347,6 @@ Check the detailed __[Setup Guide](setup.md)__ * You can leverage the [Azure Governance Visualizer Accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) to automate the deployment process. - ## Technical documentation ### Permissions overview From 4ef0e99299112cb00afe42245990557af3a58d30 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Tue, 9 Jan 2024 23:09:15 +0000 Subject: [PATCH 08/18] Readme checkpoint --- README.md | 424 +++++++++++++++++++++++++++--------------------------- 1 file changed, 212 insertions(+), 212 deletions(-) diff --git a/README.md b/README.md index fa131e6c..e7673df2 100644 --- a/README.md +++ b/README.md @@ -19,44 +19,25 @@ Azure Governance Visualizer is intended to help you to get a holistic overview o ![ConnectingDot](img/AzGovVizConnectingDots_v4.2.png) -## Azure Governance Visualizer @ Microsoft CAF & WAF - -### Microsoft Cloud Adoption Framework (CAF) - -* Listed as [tool](https://learn.microsoft.com/azure/cloud-adoption-framework/resources/tools-templates#govern) for the Govern discipline in the Microsoft Cloud Adoption Framework. -* Included in the Cloud Adoption Framework's [Strategy-Plan-Ready-Governance](https://azuredevopsdemogenerator.azurewebsites.net/?name=strategyplan) Azure DevOps Demo Generator template. - -### Microsoft Well Architected Framework (WAF) - -* Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/architecture/framework/security/monitor-tools) in the Microsoft Well Architected Framework - -### Azure Governance Visualizer accelerator - -The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) provides an easy and fast deployment process that automates the creation and publishing of AzGovViz to an Azure Web Application and provides automation to configuring the pre-requisites for AzGovViz. - -## ChatGPT - -![ChatGPT](img/chatGPT.png) - -## Content +## Table of contents * [Azure Governance Visualizer aka AzGovViz](#azure-governance-visualizer-aka-azgovviz) * [Mission](#mission) + * [Table of contents](#table-of-contents) * [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) * [Azure Governance Visualizer accelerator](#azure-governance-visualizer-accelerator) * [ChatGPT](#chatgpt) - * [Content](#content) + * [:rocket: Azure Governance Visualizer deployment guide](#rocket-azure-governance-visualizer-deployment-guide) * [Release history](#release-history) * [Demo](#demo) - * [Media](#media) - * [Slideset](#slideset) + * [Media](#media) + * [Presentations](#presentations) * [Features](#features) * [Screenshots](#screenshots) * [Outputs](#outputs) * [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) @@ -67,7 +48,7 @@ The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Gov * [Integrate with AzOps](#integrate-with-azops) * [Integrate PSRule for Azure](#integrate-psrule-for-azure) * [Stats](#stats) - * [How/What?](#howwhat) + * [How / What?](#how--what) * [Security](#security) * [Known issues](#known-issues) * [Facts](#facts) @@ -76,6 +57,31 @@ The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Gov * [AzADServicePrincipalInsights](#azadserviceprincipalinsights) * [Closing Note](#closing-note) +## Azure Governance Visualizer @ Microsoft CAF & WAF + +### Microsoft Cloud Adoption Framework (CAF) + +* Listed as [tool](https://learn.microsoft.com/azure/cloud-adoption-framework/resources/tools-templates#govern) for the Govern discipline in the Microsoft Cloud Adoption Framework. +* Included in the Cloud Adoption Framework's [Strategy-Plan-Ready-Governance](https://azuredevopsdemogenerator.azurewebsites.net/?name=strategyplan) Azure DevOps Demo Generator template. + +### Microsoft Well Architected Framework (WAF) + +* Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/architecture/framework/security/monitor-tools) in the Microsoft Well Architected Framework + +### Azure Governance Visualizer accelerator + +The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) provides an easy and fast deployment process that automates the creation and publishing of AzGovViz to an Azure Web Application and provides automation to configuring the pre-requisites for AzGovViz. + +## ChatGPT + +![ChatGPT](img/chatGPT.png) + +## :rocket: Azure Governance Visualizer deployment guide + +The instructions to deploy the Azure Governance Visualizer is found in the __[Azure Governance Visualizer (AzGovViz) deployment guide](setup.md)__. Follow those instructions to run AzGovViz from your terminal, Azure DevOps, or GitHub. + +Additionally, you can use the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) to provide HTML output access through Azure Web Apps. + ## Release history __Changes__ (2024-Jan-08 / 6.3.7 Minor) @@ -92,13 +98,13 @@ __Changes__ (2024-Jan-08 / 6.3.7 Minor) More [demo output](https://github.com/JulianHayward/AzGovViz) -### Media +## 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 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 +### Presentations Short presentation on Azure Governance Visualizer: [download](slides/AzGovViz_intro.pdf) @@ -194,108 +200,116 @@ Short presentation on Azure Governance Visualizer: [download](slides/AzGovViz_in * QuotaId * Role assignment limit * Tags - * Owner & User Access Administrator Role assignment count (at scope) direct and indirect plus PIM eligibility count - * Microsoft Defender for Cloud Secure Score - * Microsoft Defender for Cloud Email notifications configuration + * Owner & User Access Administrator role assignment count (at scope) direct and indirect plus PIM eligibility count + * Microsoft Defender for Cloud secure score + * Microsoft Defender for Cloud email notifications configuration * Cost - * Management Group path - * Tag Name usage - * Insights on usage of Tag Names on Subscriptions, ResourceGroups and Resources + * Management group path + * Tag name usage + * Insights on usage of tag names on subscriptions, resource groups, and resources * Resources - * Resource Types - * ResourceType count per location - * Resource Provider - * Resource Provider state aggregation throughout all Subscriptions - * Explicit Resource Provider state per Subscription - * Resource Locks - * Aggregated insights for Lock and respective Lock-type usage on Subscriptions, ResourceGroups and Resources + * Resource types + * Resource type count per location + * Resource provider + * Resource provider state aggregation throughout all subscriptions + * Explicit resource provider state per subscription + * Resource locks + * Aggregated insights for lock and respective lock-type usage on subscriptions, resource groups, and resources * CSV output detailed / each scope that has a lock applied (at scope) * 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) * 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 + * 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 + * Cloud Adoption Framework (CAF) [Abbreviation examples for Azure resources](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations) alignment checks * Microsoft Defender for Cloud - * Summary of Microsoft Defender for Cloud coverage by plan (count of Subscription per plan/tier) - * Summary of Microsoft Defender for Cloud plans coverage by Subscription (plan/tier) + * Summary of Microsoft Defender for Cloud coverage by plan (count of subscription per plan/tier) + * Summary of Microsoft Defender for Cloud plans coverage by subscription (plan/tier) * Highlight the usage of deprecated Defender plans (e.g. Container Registry & Kubernetes) - * UserAssigned Managed Identities assigned to Resources / vice versa - * Summary of all UserAssigned Managed Identities assigned to Resources - * Summary of Resources that have an UserAssigned Managed Identity assigned + * User-assigned managed identities assigned to resources / vice versa + * Summary of all user-assigned managed identities assigned to resources + * Summary of resources that have a user-assigned managed identity assigned * [Integrate PSRule for Azure](#integrate-psrule-for-azure) - * __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. + * __Pausing 'PSRule for Azure' integration__. Azure Governance Visualizer uses 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) + * Azure Storage account access analysis + * Provides insights on Storage accounts with focus on anonymous access (containers/blobs and 'Static website' feature) * __Network__ - * Virtual Networks + * Virtual networks * Subnets - * Virtual Network Peerings - * Private Endpoints + * Virtual network peerings + * Private endpoints * __Diagnostics__ - * Management Groups Diagnostic settings report - * Management Group, Diagnostic setting name, target type (LA, SA, EH), target Id, Log Category status - * Subscriptions Diagnostic settings report - * Subscription, Diagnostic setting name, target type (LA, SA, EH), target Id, Log Category status - * Resources Diagnostic capabilty report (1st party Resource types only) - * ResourceType capability for Resource Diagnostics including - * ResourceType count and information if capable for logs including list of available og categories - * ResourceType count and information if capable for metrics - * Lifecyle recommendations for existing Azure Policy definitions that configure Resource diagnostics of type=Log - * Check if Policy definitions hold the latest set of applicable log categories - * Recommendation to create Policy definition for ResourceType if supported - * Lists all PolicyDefinitions that deploy Resource diagnostics of type=log, lists Policy assignments and PolicySet assignments if the Policy defintion is used in a PolicySet definition + * Management groups diagnostic settings report + * Management group, diagnostic setting name, target type (Log Analytics, Storage account, Event Hub), target resource ID, log category status + * Subscriptions diagnostic settings report + * Subscription, diagnostic setting name, target type (Log Analytics, Storage account, Event Hub), target resource ID, log category status + * Resources diagnostic capability report (1st party resource types only) + * Resource type capability for resource diagnostics including: + * Resource type count and information if capable for logs including list of available og categories + * Resource type count and information if capable for metrics + * Lifecycle recommendations for existing Azure Policy definitions that configure resource diagnostics of type=Log + * Check if policy definitions hold the latest set of applicable log categories + * Recommendation to create policy definition for resource type if supported + * Lists all policy definitions that deploy resource diagnostics of type=log, lists policy assignments and PolicySet assignments if the policy definition is used in a PolicySet definition * __Limits__ * Tenant approaching ARM limits: - * Custom Role definitions + * Custom role definitions * PolicySet definitions - * Management Groups approaching ARM limits: + * Management groups approaching ARM limits: * Policy assignment limit * Policy / PolicySet definition scope limit * Role assignment limit * Subscriptions approaching ARM limits: - * ResourceGroup limit - * Subscription Tags limit + * Resource group limit + * Subscription tags limit * Policy assignment limit * Policy / PolicySet definition scope limit * Role assignment limit -* __Microsoft Entra ID (AAD)__ - * Insights on those Service Principals where a Role assignment exists (scopes: Management Group, Subscription, ResourceGroup, Resource): +* __Microsoft Entra ID__ + * Insights on those service principals where a role assignment exists (scopes: management group, subscription, resource group, and resource): * Type=ManagedIdentity - * Core information on the Service Principal such as related Ids, use case information and Role assignments - * For UserManaged Identities the count of assignment to Resources is reported - * Orphaned Managed Identity - Policy assignment related Managed Identities / the related Policy assignment does not exist - * UserAssigned Managed Identity - count of Resources that it is assigned to + * Core information on the service principal such as related IDs, use case information and role assignments + * For user-managed identities the count of assignment to resources is reported + * Orphaned managed identity - policy assignment related managed identities / the related policy assignment does not exist + * User-assigned managed identity - count of resources that it is assigned to * Type=Application * Secrets and Certificates expiry information & warning - * Report on external Service Principals + * Report on external service principals * __Consumption__ - * Aggregated consumption insights throughout the entirety of scopes (Management Groups, Subscriptions) + * Aggregated consumption insights throughout the entirety of scopes (management groups, subscriptions) * __Change tracking__ * Policy - * Created/Updated Policy and PolicySet definitions (system metadata 'createdOn, createdBy, updatedOn, updatedBy') - * Created/Updated Policy assignments (system metadata 'createdOn, createdBy, updatedOn, updatedBy') + * Created/Updated policy and PolicySet definitions (system metadata 'createdOn, createdBy, updatedOn, updatedBy') + * Created/Updated policy assignments (system metadata 'createdOn, createdBy, updatedOn, updatedBy') * RBAC - * Created/Updated Role definitions (system metadata 'createdOn, createdBy, updatedOn, updatedBy') - * Created Role assignments (system metadata 'createdOn, createdBy) + * Created/Updated role definitions (system metadata 'createdOn, createdBy, updatedOn, updatedBy') + * Created role assignments (system metadata 'createdOn, createdBy) * Resources - * Aggregated insights on Created/Changed Resources + * Aggregated insights on created/changed resources ## Screenshots HTML file __HierarchyMap__ -![alt text](img/HierarchyMap.png "HierarchyMap") + +![HierarchyMap](img/HierarchyMap.png) + __TenantSummary__ -![alt text](img/TenantSummary_20221129.png "TenantSummary") + +![TenantSummary](img/TenantSummary_20221129.png) + __DefinitionInsights__ -![alt text](img/DefinitionInsights.png "DefinitionInsights") + +![DefinitionInsights](img/DefinitionInsights.png) + __ScopeInsights__ -![alt text](img/ScopeInsights.png "ScopeInsights") + +![ScopeInsights](img/ScopeInsights.png) + *_IDs from screenshot are randomized_ markdown in Azure DevOps Wiki as Code @@ -309,66 +323,51 @@ markdown in Azure DevOps Wiki as Code * CSV file * HTML file - * the HTML file uses Java Script and CSS files which are hosted on various CDNs (Content Delivery Network). For details review the BuildHTML region in the PowerShell script file. - * Browsers tested: Edge, new Edge and Chrome + * the HTML file uses JavaScript and CSS files which are hosted on various CDNs (Content Delivery Network). For details review the BuildHTML region in the PowerShell script file. + * Browsers tested: Edge and Chrome * MD (Markdown) file - * for use with Azure DevOps Wiki leveraging the [Mermaid](https://docs.microsoft.com/en-us/azure/devops/release-notes/2019/sprint-158-update#mermaid-diagram-support-in-wiki) plugin + * for use with Azure DevOps Wiki using the [Mermaid](https://learn.microsoft.com/azure/devops/release-notes/2019/sprint-158-update#mermaid-diagram-support-in-wiki) plugin * JSON folder ([demo-output](https://github.com/JulianHayward/AzGovViz)) containing - * all Policy and Role assignments (Scopes: Tenant, Management Groups and Subscriptions) - * all BuiltIn and Custom Policy/Set definitions (Scopes: Management Groups and Subscriptions) - * all BuiltIn and Custom Role definitions - * JSON file of ManagementGroup Hierarchy including all Custom Policy/Set and RBAC definitions, Policy and Role assignments and some more relevant information - * Tenant tree including all Policy and Role assignments AND all Custom Policy/Set and Role definitions - ![alt text](img/jsonfolderfull450.jpg "JSONFolder") + * all policy and role assignments (Scopes: tenant, management groups, and subscriptions) + * all built-in and custom policy & policy set definitions (Scopes: management groups and subscriptions) + * all built-in and custom role definitions + * JSON file of management group hierarchy including all custom policy, policy set, and RBAC definitions, policy and role assignments and some more relevant information + * Tenant tree including all policy and role assignments and all custom policy & policy set and role definitions + + ![JSONFolder](img/jsonfolderfull450.jpg) ## 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. +_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 use 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. +Setup a Virtual Machine in Azure, deploy the dependency agent extension and [execute](setup.md#set-up-and-run-azure-governance-visualizer-from-the-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. +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") +![Insights on pwsh](img/insights_map_pwsh.png) Query for Log Analytics: -``` +```kusto 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)__ - -* You can leverage the [Azure Governance Visualizer Accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) to automate the deployment process. - ## Technical documentation ### Permissions overview -![alt text](img/permissions.png "example output") +![example output](img/permissions.png) ### Required permissions in Azure -This permission is mandatory in each and every scenario! +These permissions are __mandatory__ in each and every scenario! - - - - - - - - - - - -
ScenarioPermissions
ANY
Console / Azure DevOps / GitHub Actions ..
Reader Role assignment on Management Group
+| Scenario | Permissions | +| :------- | :---------- | +| ALL | '__Reader__' role assignment on _management group_ | ### Required permissions in Microsoft Entra ID @@ -380,48 +379,48 @@ This permission is mandatory in each and every scenario! A
Console | Member user account - No Microsoft Entra ID (AAD) permissions required + No Microsoft Entra ID permissions required B
Console | Guest user account If the tenant is hardened (Microsoft Entra ID (AAD) External Identities / Guest user access = most restrictive) then Guest User must be assigned the Microsoft Entra ID (AAD) Role 'Directory readers'
- 💡 Compare member and guest default permissions
- 💡 Restrict Guest permissions + 💡 Compare member and guest default permissions
+ 💡 Restrict guest access permissions in Microsoft Entra ID - C
Console | Service Principal | Managed Identity + C
Console | Service principal | Managed identity - + - - + + - - + + - - + + - - + +
CapabilityAPI PermissionsMicrosoft Graph API permissions
Get Microsoft Entra ID (AAD)
Users
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / User / User.Read.All
💡 Get user
Get Microsoft Entra ID
Users
Service principal's App registration
grant with Microsoft Graph permissions:
Application permissions / User / User.Read.All
💡 Get user
Get Microsoft Entra ID (AAD)
Groups
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / Group / Group.Read.All
💡 Get group
Get Microsoft Entra ID
Groups
Service principal's App registration
grant with Microsoft Graph permissions:
Application permissions / Group / Group.Read.All
💡 Get group
Get Microsoft Entra ID (AAD)
SP/App
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / Application / Application.Read.All
💡 Get servicePrincipal, Get application
Get Microsoft Entra ID
SP/App
Service principal's App registration
grant with Microsoft Graph permissions:
Application permissions / Application / Application.Read.All
💡 Get servicePrincipal, Get application
Get PIM Eligibility
SP/App
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / PrivilegedAccess / PrivilegedAccess.Read.AzureResources
💡 Get privilegedAccess for AzureResources
If you cannot grant this permission then use parameter -NoPIMEligibility
Get PIM eligibility
SP/App
Service principal's App registration
grant with Microsoft Graph permissions:
Application permissions / PrivilegedAccess / PrivilegedAccess.Read.AzureResources
💡 Get privilegedAccess for Azure resources
If you cannot grant this permission then use parameter -NoPIMEligibility
- Optional: Microsoft Entra ID (AAD) Role 'Directory readers' could be used instead of API permissions (more 'read' than required) + Optional: Microsoft Entra ID role 'Directory readers' could be used instead of API permissions (more 'read' than required) - D
Azure DevOps / Github Actions | ServicePrincipal + D
Azure DevOps / Github Actions | Service principal @@ -430,43 +429,42 @@ This permission is mandatory in each and every scenario! - - + + - - + + - - + + - - + +
API Permissions
Get Microsoft Entra ID (AAD)
Users
Azure DevOps Service Connection's App registration
grant with Microsoft Graph permissions:
Application permissions / User / User.Read.All
💡 Get user
Get Microsoft Entra ID
Users
Azure DevOps service connection's App registration
grant with Microsoft Graph permissions:
Application permissions / User / User.Read.All
💡 Get user
Get Microsoft Entra ID (AAD)
Groups
Azure DevOps Service Connection's App registration
grant with Microsoft Graph permissions:
Application permissions / Group / Group.Read.All
💡 Get group
Get Microsoft Entra ID
Groups
Azure DevOps service connection's App registration
grant with Microsoft Graph permissions:
Application permissions / Group / Group.Read.All
💡 Get group
Get Microsoft Entra ID (AAD)
SP/App
Azure DevOps Service Connection's App registration
grant with Microsoft Graph permissions:
Application permissions / Application / Application.Read.All
💡 Get servicePrincipal, Get application
Get Microsoft Entra ID
SP/App
Azure DevOps service connection's App registration
grant with Microsoft Graph permissions:
Application permissions / Application / Application.Read.All
💡 Get service principal, Get application
Get PIM Eligibility
SP/App
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / PrivilegedAccess / PrivilegedAccess.Read.AzureResources
💡 Get privilegedAccess for AzureResources
If you cannot grant this permission then use parameter -NoPIMEligibility
Get PIM eligibility
SP/App
Service principal's App registration
grant with Microsoft Graph permissions:
Application permissions / PrivilegedAccess / PrivilegedAccess.Read.AzureResources
💡 Get privilegedAccess for Azure resources
If you cannot grant this permission then use parameter -NoPIMEligibility
- Optional: Microsoft Entra ID (AAD) Role 'Directory readers' could be used instead of API permissions (more 'read' than required) + Optional: Microsoft Entra ID role 'Directory readers' could be used instead of API permissions (more 'read' than required) -Screenshot Azure Portal -![alt text](img/aadpermissionsportal_4.jpg "Permissions in Microsoft Entra ID") +Screenshot of Microsoft Graph permissions in the Microsoft Entra admin center + +![Screenshot of Microsoft Graph permissions in the Microsoft Entra admin center](img/aadpermissionsportal_4.jpg) ### PowerShell * Requires PowerShell 7 (minimum supported version 7.0.3) * [Get PowerShell](https://github.com/PowerShell/PowerShell#get-powershell) - * [Installing PowerShell on Windows](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows) - * [Installing PowerShell on Linux](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux) + * [Installing PowerShell on Windows](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows) + * [Installing PowerShell on Linux](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-linux) * Requires PowerShell Az Modules * Az.Accounts - * ~~Az.Resources~~ - * ~~Az.ResourceGraph~~ - * [Install the Azure Az PowerShell module](https://docs.microsoft.com/en-us/powershell/azure/install-az-ps) + * [Install the Azure Az PowerShell module](https://learn.microsoft.com/powershell/azure/install-azure-powershell) * Requires PowerShell Module 'AzAPICall' * Running in Azure DevOps or GitHub Actions the required AzAPICall module version will be installed automatically * Running from Console the script will prompt you to confirm installation of the required AzAPICall module version @@ -608,24 +606,24 @@ Azure Governance Visualizer 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). +Did you know you can run AzOps from Azure DevOps? Check [AzOps Accelerator](https://github.com/Azure/AzOps-Accelerator). You can integrate Azure Governance Visualizer (same project as AzOps). ```yaml - pipelines: - - pipeline: 'Push' - source: 'AzOps - Push' - trigger: - branches: - include: - - master +pipelines: + - pipeline: 'Push' + source: 'AzOps - Push' + trigger: + branches: + include: + - master ``` ## Integrate PSRule for Azure -__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. +__Pausing 'PSRule for Azure' integration__. Azure Governance Visualizer used 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. +Let's use [PSRule for Azure](https://azure.github.io/PSRule.Rules.Azure) and use 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. Parameter: `-DoPSRule` (e.g. `.\pwsh\AzGovVizParallel.ps1 -DoPSRule`) @@ -644,67 +642,68 @@ Outputs: * CSV (detailed, per resource) TenantSummary HTML output example: -![alt text](img/PSRuleForAzure_preview.png "PSRule for Azure / Azure Governance Visualizer TenantSummary") + +![PSRule for Azure / Azure Governance Visualizer TenantSummary](img/PSRuleForAzure_preview.png) ## Stats 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? +### How / What? -If the script is run in Azure DevOps then the Repository Id and executing principal´s object Id will be used to create an unique identifier. -If the script is not run in Azure DevOps then the Tenant Id and executing principal´s object Id will be used to create an unique identifier. +If the script is run in Azure DevOps then the repository ID and executing principal's object ID will be used to create an unique identifier. +If the script is not run in Azure DevOps then the tenant ID and executing principal's object ID will be used to create an unique identifier. -SHA384/512 hashed combination of +SHA384/512 hashed combination of: * portion of the repositoryId/tenantId - * if repositoryId/tenantId startsWith a letter then use characters 3-8 (6 characters) of the first GUID´s block, combine them with the third GUID`s block of the principal´s objectId (4 characters), SHA512 hash them as identifier0 - * if repositoryId/tenantId startsWith a number then use characters 7-12 (6 characters) of the last GUID`s block, combine them with the second GUID´s block of the principal´s objectId (4 characters), SHA384 hash them as identifier0 + * if repositoryId/tenantId starts with a letter then use characters 3-8 (6 characters) of the first GUID's block, combine them with the third GUID's block of the principal´s objectId (4 characters), SHA512 hash them as identifier0 + * if repositoryId/tenantId starts with a number then use characters 7-12 (6 characters) of the last GUID's block, combine them with the second GUID´s block of the principal´s objectId (4 characters), SHA384 hash them as identifier0 * portion of the executing principal´s objectId - * if objectId startsWith a letter then use characters 3-8 (6 characters) of the first GUID´ block, combine them with the third GUID´ block of the repositoryId/tenantId (4 characters), SHA512 hash them as identifier1 - * if objectId startsWith a number then use characters 7-12 (6 characters) of the last GUID´ block, combine them with the second GUID´ block of the repositoryId/tenantId (4 characters), SHA384 hash them as identifier1 + * if objectId starts with a letter then use characters 3-8 (6 characters) of the first GUID's block, combine them with the third GUID's block of the repositoryId/tenantId (4 characters), SHA512 hash them as identifier1 + * if objectId starts with a number then use characters 7-12 (6 characters) of the last GUID's block, combine them with the second GUID's block of the repositoryId/tenantId (4 characters), SHA384 hash them as identifier1 Combine identifier0 and identifier1 -* if objectId startsWith a letter then combine identifiers -> 'identifier0 + identifier1', SHA512 hash them as final identifier and remove dashes (string of 128 characters) -* if objectId startsWith a number then combine identifiers -> 'identifier1 + identifier0', SHA512 hash them as final identifier and remove dashes (string of 128 characters) +* if objectId starts with a letter then combine identifiers -> 'identifier0 + identifier1', SHA512 hash them as final identifier and remove dashes (string of 128 characters) +* if objectId starts with a number then combine identifiers -> 'identifier1 + identifier0', SHA512 hash them as final identifier and remove dashes (string of 128 characters) -To conclude the approach: taking 6 or 4 characters from tenantId/respositoryId and objectId of the executing principal to create a unique identifier, which may not be backward resolveable. +To conclude the approach: taking 6 or 4 characters from tenant ID / respository ID and object ID of the executing principal to create a unique identifier, which may not be backward resolveable. -![alt text](img/identifier.jpg "identifier") +![identifier](img/identifier.jpg) The following data will be ingested to Azure Application Insights: -``` - "accType": "ServicePrincipal / User (member) / User (Guest)", - "azCloud": "Azure environment e.g. AzureCloud, ChinaCloud, etc.", - "identifier": "8c62a7..53d08c0 (string of 128 characters)", - "platform": "Console / AzureDevOps", - "productVersion": "used Azure Governance Visualizer version", - "psAzAccountsVersion": "used Az.Accounts PS module version", - "psVersion": "used PowerShell version", - "scopeUsage": "childManagementGroup / rootManagementGroup", - "statsCountErrors": "count of encountered errors", - "statsCountSubscriptions": "less than 100 / more than 100 (no exact numbers)", - "statsParametersDoNotIncludeResourceGroupsAndResourcesOnRBAC": "true / false", - "statsParametersDoNotIncludeResourceGroupsOnPolicy": "true / false", - "statsParametersDoNotShowRoleAssignmentsUserData": "true / false", - "statsParametersHierarchyMapOnly": "true / false", - "statsParametersLargeTenant": "true / false", - "statsParametersNoASCSecureScore" "true / false", - "statsParametersNoAzureConsumption": "true / false", - "statsParametersNoJsonExport": "true / false", - "statsParametersNoPolicyComplianceStates": "true / false", - "statsParametersNoResourceProvidersDetailed": "true / false", - "statsParametersNoResources": "true / false", - "statsParametersPolicyAtScopeOnly": "true / false", - "statsParametersRBACAtScopeOnly": "true / false", - "statsTry": "count of try sending to Application Insights" +```text +"accType": "ServicePrincipal / User (member) / User (Guest)", +"azCloud": "Azure environment e.g. AzureCloud, ChinaCloud, etc.", +"identifier": "8c62a7..53d08c0 (string of 128 characters)", +"platform": "Console / AzureDevOps", +"productVersion": "used Azure Governance Visualizer version", +"psAzAccountsVersion": "used Az.Accounts PS module version", +"psVersion": "used PowerShell version", +"scopeUsage": "childManagementGroup / rootManagementGroup", +"statsCountErrors": "count of encountered errors", +"statsCountSubscriptions": "less than 100 / more than 100 (no exact numbers)", +"statsParametersDoNotIncludeResourceGroupsAndResourcesOnRBAC": "true / false", +"statsParametersDoNotIncludeResourceGroupsOnPolicy": "true / false", +"statsParametersDoNotShowRoleAssignmentsUserData": "true / false", +"statsParametersHierarchyMapOnly": "true / false", +"statsParametersLargeTenant": "true / false", +"statsParametersNoASCSecureScore" "true / false", +"statsParametersNoAzureConsumption": "true / false", +"statsParametersNoJsonExport": "true / false", +"statsParametersNoPolicyComplianceStates": "true / false", +"statsParametersNoResourceProvidersDetailed": "true / false", +"statsParametersNoResources": "true / false", +"statsParametersPolicyAtScopeOnly": "true / false", +"statsParametersRBACAtScopeOnly": "true / false", +"statsTry": "count of try sending to Application Insights" ``` Azure Application Insights data: -![alt text](img/stats.jpg "Stats") +![Stats](img/stats.jpg) If you do not want to contribute to stats for Azure Governance Visualizer then you can use the parameter: `-StatsOptOut` @@ -717,29 +716,30 @@ Thank you for your support! ✋ __Take care__: 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!__ -☝ __Be aware__: Any _member_ user of the tenant can execute/run the script against the Management Group (and below) if the _member_ user has the RBAC Role 'Reader' assigned at Management Group (this of course also applies for the root Management Group). More important: also _guest_ users can execute/run the script if your tenant is not hardened (and has the RBAC Role 'Reader' assigned at Management Group) __Microsoft Entra Id (AAD) | External Identities | External collaboration settings | Guest user access__ [ref](https://learn.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions) +☝ __Be aware__: Any _member_ user of the tenant can execute/run the script against the management group (and below) if the _member_ user has the Azure RBAC role 'Reader' assigned at management froup (this of course also applies for the root management group). More important: also _guest_ users can execute/run the script if your tenant is not hardened (and has the RBAC role 'Reader' assigned at management group) __Microsoft Entra Id | External Identities | External collaboration settings | Guest user access__ [ref](https://learn.microsoft.com/entra/identity/users/users-restrict-guest-permissions) 🛡️ __Collaborate with the security team__: 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") + +![Microsoft defender for Cloud security alert](img/azgvz_MDfC_securityAlert.png) ## Known issues Working with Git and Windows cloning from your AzDO repository you may experience the following error: -``` +```output fatal: cannot create directory at 'output/JSON_...': Filename too long ``` To work around that issue you may want to enable longpaths support. __Note the [caveats](https://github.com/desktop/desktop/issues/8023)!__ -``` +```shell git config --system core.longpaths true ``` ## Facts -Disabled Subscriptions and Subscriptions where Quota Id starts with with "AAD_" are being skipped, all others are queried. More information on Subscription Quota Id / Offer numbers: [Supported Microsoft Azure offers](https://docs.microsoft.com/en-us/azure/cost-management-billing/costs/understand-cost-mgt-data#supported-microsoft-azure-offers). +Disabled Azure subscriptions and subscriptions where Quota ID starts with with "AAD_" are being skipped, all others are queried. More information on Subscription Quota ID / Offer numbers: [Supported Microsoft Azure offers](https://learn.microsoft.com/azure/cost-management-billing/costs/understand-cost-mgt-data#supported-microsoft-azure-offers). ARM Limits are not acquired programmatically, these are hardcoded. The links used to check related limits are commented in the param section of the script. @@ -771,16 +771,16 @@ Kudos to @ElanShudnow for [AzSubnetAvailability - GitHub](https://github.com/Ela ## AzAdvertizer -![alt text](img/azadvertizer70.png "example output") +![AzAdvertizer logo](img/azadvertizer70.png) -Also check - AzAdvertizer helps you to keep up with the pace by providing overview and insights on new releases and changes/updates for Azure Governance capabilities such as Azure Policy's Policy definitions, initiatives (Set definitions), aliases and Azure RBAC's Role definitions and resource provider operations. +Also check - AzAdvertizer helps you to keep up with the pace by providing overview and insights on new releases and changes/updates for Azure governance capabilities such as Azure Policy's policy definitions, initiatives (set definitions), aliases, and Azure RBAC's role definitions and resource provider operations. ## AzADServicePrincipalInsights -Also check - What about your Microsoft Entra ID Service Principals? Get deep insights and track your Service Principals with AzADServicePrincipalInsights. Create a HTML overview, export to CSV and JSON and use it for further processing... +Also check - What about your Microsoft Entra ID service principals? Get deep insights and track your service principals with AzADServicePrincipalInsights. Create an HTML overview, export to CSV and JSON and use it for further processing. -![alt text](img/azadserviceprincipalinsights_preview_entra-id.png "example output") +![example output](img/azadserviceprincipalinsights_preview_entra-id.png) ## Closing Note -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. +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 no implicit or explicit obligations related to this project, it is provided 'as is' with no warranties and confer no rights. From 351a357a3edf1aa137276c2531634fd80ff2c6db Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 10 Jan 2024 14:21:17 +0000 Subject: [PATCH 09/18] URL updates --- README.md | 14 +- history.md | 32 +-- pwsh/AzGovVizParallel.ps1 | 184 +++++++++--------- pwsh/dev/devAzGovVizParallel.ps1 | 182 ++++++++--------- .../dataCollectionFunctions.ps1 | 6 +- pwsh/dev/functions/getConsumption.ps1 | 8 +- .../functions/getDefaultManagementGroup.ps1 | 2 +- pwsh/dev/functions/getMDfCSecureScoreMG.ps1 | 2 +- pwsh/dev/functions/getOrphanedResources.ps1 | 2 +- pwsh/dev/functions/getPolicyRemediation.ps1 | 2 +- pwsh/dev/functions/html/htmlFunctions.ps1 | 2 +- pwsh/dev/functions/processNetwork.ps1 | 2 +- .../functions/processScopeInsightsMgOrSub.ps1 | 34 ++-- pwsh/dev/functions/processTenantSummary.ps1 | 116 +++++------ run-from/console.md | 2 +- setup.md | 2 +- 16 files changed, 295 insertions(+), 297 deletions(-) diff --git a/README.md b/README.md index e7673df2..96b60d39 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Azure Governance Visualizer is intended to help you to get a holistic overview o ### Microsoft Well Architected Framework (WAF) -* Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/architecture/framework/security/monitor-tools) in the Microsoft Well Architected Framework +* Listed as [security monitoring tool](https://learn.microsoft.com/azure/architecture/framework/security/monitor-tools) in the Microsoft Well Architected Framework ### Azure Governance Visualizer accelerator @@ -498,7 +498,7 @@ Screenshot of Microsoft Graph permissions in the Microsoft Entra admin center * `-AzureConsumptionPeriod` - Define for which time period Azure Consumption data should be gathered; default is 1 day * `-NoAzureConsumptionReportExportToCSV` - Azure Consumption data should not be exported (CSV) * `-NoScopeInsights` - Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size). Use `-LargeTenant` to further reduce the output. -* `-ThrottleLimit` - leveraging PowerShell´s parallel capability you can define the ThrottleLimit (default=5) +* `-ThrottleLimit` - leveraging PowerShell's parallel capability you can define the ThrottleLimit (default=5) * `-DoTranscript` - Log the console output * `-SubscriptionId4AzContext` - Define the Subscription Id to use for AzContext (default is to use a random Subscription Id) * `-PolicyAtScopeOnly` - Removing 'inherited' lines in the HTML file for 'Policy Assignments'; use this parameter if you run against a larger tenants. Note using parameter `-LargeTenant` will set `-PolicyAtScopeOnly $true` @@ -624,7 +624,7 @@ pipelines: __Pausing 'PSRule for Azure' integration__. Azure Governance Visualizer used 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 use 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. +PSRule for Azure is listed as [security monitoring tool](https://learn.microsoft.com/azure/architecture/framework/security/monitor-tools) in the Microsoft Well-Architected Framework. Parameter: `-DoPSRule` (e.g. `.\pwsh\AzGovVizParallel.ps1 -DoPSRule`) Optional parameters: @@ -657,9 +657,9 @@ If the script is not run in Azure DevOps then the tenant ID and executing princi SHA384/512 hashed combination of: * portion of the repositoryId/tenantId - * if repositoryId/tenantId starts with a letter then use characters 3-8 (6 characters) of the first GUID's block, combine them with the third GUID's block of the principal´s objectId (4 characters), SHA512 hash them as identifier0 - * if repositoryId/tenantId starts with a number then use characters 7-12 (6 characters) of the last GUID's block, combine them with the second GUID´s block of the principal´s objectId (4 characters), SHA384 hash them as identifier0 -* portion of the executing principal´s objectId + * if repositoryId/tenantId starts with a letter then use characters 3-8 (6 characters) of the first GUID's block, combine them with the third GUID's block of the principal's objectId (4 characters), SHA512 hash them as identifier0 + * if repositoryId/tenantId starts with a number then use characters 7-12 (6 characters) of the last GUID's block, combine them with the second GUID's block of the principal's objectId (4 characters), SHA384 hash them as identifier0 +* portion of the executing principal's objectId * if objectId starts with a letter then use characters 3-8 (6 characters) of the first GUID's block, combine them with the third GUID's block of the repositoryId/tenantId (4 characters), SHA512 hash them as identifier1 * if objectId starts with a number then use characters 7-12 (6 characters) of the last GUID's block, combine them with the second GUID's block of the repositoryId/tenantId (4 characters), SHA384 hash them as identifier1 @@ -749,7 +749,7 @@ 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 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 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 Azure Governance Visualizer outputs executed on his implementation of EnterpriseScale. diff --git a/history.md b/history.md index bedeb799..7e47bd9a 100644 --- a/history.md +++ b/history.md @@ -27,7 +27,7 @@ __Changes__ (2023-Nov-13 / 6.3.4 Minor) * tolerating more up to date AzAPICall version when executing outside of Azure DevOps/GitHub * update ARM API-version for Resources. Using `2023-07-01` instead of `2021-04-01` * update `/.azuredevops/pipelines/AzGovViz.variables.yml` - * add parameter `-ARMLocation` + * add parameter `-ARMLocation` * update README.md * update [API reference](#api-reference) * use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.84 @@ -37,7 +37,7 @@ __Changes__ (2023-Oct-22 / 6.3.3 Minor) * introduce new optional parameter `-AzAPICallSkipAzContextSubscriptionValidation` [ref](https://aka.ms/AzAPICall) * update ARM API-version for RBAC Role definitions. Using `2022-05-01-preview` instead of `2018-11-01-preview`. This will show us 'conditions' [example](https://www.azadvertizer.net/azrolesadvertizer/8b54135c-b56d-4d72-a534-26097cfdc8d8.html) * update `/.azuredevops/pipelines/AzGovViz.variables.yml` - * add parameter `-AzAPICallSkipAzContextSubscriptionValidation` + * add parameter `-AzAPICallSkipAzContextSubscriptionValidation` * structure AzAPICall related variables * Azure Active Directory becomes Microsoft Entra ID * update README.md and setup.md @@ -59,7 +59,7 @@ __Changes__ (2023-Sep-04 / 6.3.1 Minor) __Changes__ (2023-Aug-02 / 6.3.0 Minor) -* workaround for [issue121](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/121); remove files hitting the GitHub file size limit [ref](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github#file-size-limits) +* workaround for [issue121](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/121); remove files hitting the GitHub file size limit [ref](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github#file-size-limits) * update GitHub workflows: * [AzGovViz_OIDC.yml](/.github/workflows/AzGovViz_OIDC.yml) * [AzGovViz.yml](/.github/workflows/AzGovViz.yml) @@ -241,7 +241,7 @@ __Changes__ (2023-Jan-24 / Major) __Changes__ (2023-Jan-19 / Major) -* Cover Preview [Azure Storage Account with Azure DNS zone endpoints](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview#azure-dns-zone-endpoints-preview) ([Issue #164](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/164)) +* Cover Preview [Azure Storage Account with Azure DNS zone endpoints](https://learn.microsoft.com/azure/storage/common/storage-account-overview#azure-dns-zone-endpoints-preview) ([Issue #164](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/164)) * Add feature to simulate Management Group Hierarchy Map * New parameter `-HierarchyMapOnlyCustomData` (documentation update pending) * Private Endpoint feature - add Microsoft tenants (cross tenant PE) (`-MSTenantIds`) @@ -281,7 +281,7 @@ __Changes__ (2022-Dec-22 / Major) * Fix issue for Private Endpoints feature * Add reference for Microsoft Defender for Cloud security alerts on AzGovViz activity - [Security](#security) -* Fix for migrated Subscriptions. In rare cases a subscription that was migrated to another tenant may still be returned from the [Entities ARM API](https://learn.microsoft.com/en-us/rest/api/managementgroups/entities/list), but not from the [Subscriptions ARM API](https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list) - if that is the case then these subscriptions will be added to the out-of-scope subscriptions collection +* Fix for migrated Subscriptions. In rare cases a subscription that was migrated to another tenant may still be returned from the [Entities ARM API](https://learn.microsoft.com/rest/api/managementgroups/entities/list), but not from the [Subscriptions ARM API](https://learn.microsoft.com/rest/api/resources/subscriptions/list) - if that is the case then these subscriptions will be added to the out-of-scope subscriptions collection * Use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.62 * Fix issue [155](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/155) AzureChinaCloud * Minor optimizations @@ -299,7 +299,7 @@ __Changes__ (2022-Dec-12 / Major) * 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. * Enhance Private Endpoints feature / cross tenant PE -* Fix for migrated Subscriptions. In rare cases a subscription that was migrated to another tenant may still be returned from the [ARM API](https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list), if that is the case then these subscriptions will be added to the out-of-scope subscriptions collection +* Fix for migrated Subscriptions. In rare cases a subscription that was migrated to another tenant may still be returned from the [ARM API](https://learn.microsoft.com/rest/api/resources/subscriptions/list), if that is the case then these subscriptions will be added to the out-of-scope subscriptions collection * Update Azure Devops Pipeline YAML * Enhance error handling if Management Group Id containing spaces is provided - thanks @cbezenco * Use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.59 @@ -502,7 +502,7 @@ __Changes__ (2022-Jul-17 / Major) __Changes__ (2022-Jul-14 / Major) -* New feature - 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 (HTML TenantSummary, ScopeInsights and CSV output) +* New feature - Cloud Adoption Framework (CAF) [Abbreviation examples for Azure resources](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations) compliance (HTML TenantSummary, ScopeInsights and CSV output) * Optimize PSRule data handling * Minor optimizations @@ -565,11 +565,11 @@ __Changes__ (2022-May-21 / Major) > Note: Azure DevOps and GitHub users must update the YAML file(s) and PowerShell files (`AzGovVizParallel.ps1` and `prerequisites.ps1`) * Integration of [PSRule for Azure](#integrate-psrule-for-azure). This feature is optional, use new parameter `-DoPSRule` - * Provides a [Azure Well-Architected Framework](https://docs.microsoft.com/en-gb/azure/architecture/framework/) aligned suite of rules for validating Azure resources + * Provides a [Azure Well-Architected Framework](https://learn.microsoft.com/azure/well-architected/) aligned suite of rules for validating Azure resources * Provides meaningful information to allow remediation * New parameter `-PSRuleVersion` - Define the PSRule..Rules.Azure PowerShell module version, if undefined then 'latest' will be used * Optional feature: publish HTML to Azure Web App (check the __[Setup Guide](setup.md)__) in Azure DevOps or GitHub Actions - thanks Wayne Meyer -* New feature / report on [enabled Subscription Features](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/preview-features) TenantSummary, ScopeInsights and CSV export +* New feature / report on [enabled Subscription Features](https://learn.microsoft.com/azure/azure-resource-manager/management/preview-features) TenantSummary, ScopeInsights and CSV export * Decomissioned Azure DevOps `.pipelines` - use the new YAML files `.azuredevops/pipelines/*` * Fix [#issue92](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/92) -> pipeline .azuredevops/pipelines/AzGovViz.pipeline.yml * Update Azure DevOps pipelines / use AzurePowershell@5 @@ -609,7 +609,7 @@ __Changes__ (2022-Apr-25 / Major) __Changes__ (2022-Jan-31 / Major) * New __TenantSummary | RBAC__ feature - insights on all Role definitions that are capable to write Role assignments -* __TenantSummary | Subscriptions, Resources & Defender | Subscriptions__ report (new) [Role assignment limits](https://docs.microsoft.com/en-us/azure/role-based-access-control/troubleshooting#azure-role-assignments-limit) +* __TenantSummary | Subscriptions, Resources & Defender | Subscriptions__ report (new) [Role assignment limits](https://learn.microsoft.com/azure/role-based-access-control/troubleshooting#azure-role-assignments-limit) * Handling orphaned Policy assignments (scope Management Group) * Datacollection for Management Groups process in batches (batch per Management Group level) * Update Dockerfile @@ -670,7 +670,7 @@ __Changes__ (2021-Nov-01 / Major) * New output - Feature request to create __Scope Insights__ output per Subscription has been implement. With this new feature you can share Subscription __Scope Insights__ with Subscription responsible staff. Use parameter `-NoSingleSubscriptionOutput` to disable the feature * Update [Required permissions in Azure Active Directory](#required-permissions-in-azure-active-directory) for the scenario of a Guest User executing the script -* Add 'daily summary' output (CSV) to easily track your Tenant´s Governance evolution over time - Tim will hopefully create a PR for how he leverages AzGovViz historical data for Azure Log Analytics based dashboards +* Add 'daily summary' output (CSV) to easily track your Tenant's Governance evolution over time - Tim will hopefully create a PR for how he leverages AzGovViz historical data for Azure Log Analytics based dashboards * Improved permission related error handling __Changes__ (2021-Oct-25 / Major) @@ -683,7 +683,7 @@ __Changes__ (2021-Oct-21 / Major) __Release v6 Changes__ -* Removed usage of Azure PowerShell cmdlet 'Get-AzRoleAssignment' / preparing for upcoming deprecation of 'Azure Active Directory Graph' API ([announcement](https://azure.microsoft.com/en-us/updates/update-your-apps-to-use-microsoft-graph-before-30-june-2022/)) +* Removed usage of Azure PowerShell cmdlet 'Get-AzRoleAssignment' / preparing for upcoming deprecation of 'Azure Active Directory Graph' API ([announcement](https://azure.microsoft.com/updates/update-your-apps-to-use-microsoft-graph-before-30-june-2022/)) * Management Group diagnostic setting - reflect inheritance of diagnostic settings from upper Management Group scopes * __TenantSummary__ Policy assignments - resolve Managed Identity (if Policy assignment effect is DeployIfNotExists (DINE) or Modify) * Removed __TenantSummary__ RBAC Classic Role assignments @@ -738,7 +738,7 @@ __Changes__ (2021-Aug-25 / Major) __Changes__ (2021-Aug-22 / Major) -* Bugfix - indirect Role assignments (applied through AAD group membership); switched to Graph beta endpoint as v1.0 only resolves users and groups, whilst we´re also interested in Service Principals - [List group transitive members](https://docs.microsoft.com/en-us/graph/api/group-list-transitivemembers) +* Bugfix - indirect Role assignments (applied through AAD group membership); switched to Graph beta endpoint as v1.0 only resolves users and groups, whilst we're also interested in Service Principals - [List group transitive members](https://learn.microsoft.com/graph/api/group-list-transitivemembers) __Changes__ (2021-Aug-18 / Major) @@ -835,12 +835,12 @@ __Breaking Changes__ (2021-Feb-28) * When granting __Azure Active Directory Graph__ API permissions in the background an AAD Role assignment for AAD Group __Directory readers__ was triggered automatically - since January/February 2021 this is no longer the case. Review the updated [__AzGovViz technical documentation__](#azgovviz-technical-documentation) section for detailed permission requirements. -__Let´s accellerate by going parallel!__ (2021-Feb-14) +__Let's accelerate by going parallel!__ (2021-Feb-14) * Support for PowerShell Core ONLY! No support for PowerShell version < 7.0.3 * New section __DefinitionInsights__ - Insights on all built-in and custom Policy, PolicySet and RBAC Role definitions * New parameter `-NoScopeInsights` - Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size) -* New parameter `-ThrottleLimit` - Leveraging PowerShell Core´s parallel capability you can define the ThrottleLimit (default=5) +* New parameter `-ThrottleLimit` - Leveraging PowerShell Core's parallel capability you can define the ThrottleLimit (default=5) * New parameter `DoTranscript` - Log the console output * Parameter `SubscriptionQuotaIdWhitelist` now expects an array * Renamed parameter `-NoServicePrincipalResolve` to `-NoAADServicePrincipalResolve` @@ -915,7 +915,7 @@ Updates 2020-Nov-19 * New parameter `-Experimental` (see [__Parameters__](#powerShell)) * Performance optimization * Error handling optimization / API -* Azure DevOps pipeline worker changed from 'ubuntu-latest' to 'ubuntu-18.04' (see [Azure Pipelines - Sprint 177 Update](https://docs.microsoft.com/en-us/azure/devops/release-notes/2020/pipelines/sprint-177-update#ubuntu-latest-pipelines-will-soon-use-ubuntu-2004), [Ubuntu-latest workflows will use Ubuntu-20.04 #1816](https://github.com/actions/virtual-environments/issues/1816)) +* Azure DevOps pipeline worker changed from 'ubuntu-latest' to 'ubuntu-18.04' (see [Azure Pipelines - Sprint 177 Update](https://learn.microsoft.com/azure/devops/release-notes/2020/pipelines/sprint-177-update#ubuntu-latest-pipelines-will-soon-use-ubuntu-2004), [Ubuntu-latest workflows will use Ubuntu-20.04 #1816](https://github.com/actions/virtual-environments/issues/1816)) Updates 2020-Nov-08 diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 7cc6a6d7..cf6dce8a 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -578,14 +578,14 @@ Param [switch] $ShowRunIdentifier, - #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits + #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits [int] $LimitRBACCustomRoleDefinitionsTenant = 5000, [int] $LimitRBACRoleAssignmentsManagementGroup = 500, - #https://docs.microsoft.com/en-us/azure/governance/policy/overview#maximum-count-of-azure-policy-objects + #https://learn.microsoft.com/azure/governance/policy/overview#maximum-count-of-azure-policy-objects [int] $LimitPOLICYPolicyAssignmentsManagementGroup = 200, @@ -613,7 +613,7 @@ Param [int] $LimitPOLICYPolicySetDefinitionsScopedSubscription = 200, - #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits + #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits [int] $LimitResourceGroups = 980, @@ -2592,7 +2592,7 @@ function getConsumption { #$subscriptionIdsOptimizedForBody = '"{0}"' -f ($subsToProcessInCustomDataCollection.subscriptionId -join '","') $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for $($subsToProcessInCustomDataCollectionCount) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' @@ -2743,7 +2743,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -2804,7 +2804,7 @@ function getConsumption { #region mgScope $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $body = @" @@ -2935,7 +2935,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -3156,7 +3156,7 @@ function getConsumption { function getDefaultManagementGroup { $currentTask = 'Get Default Management Group' Write-Host $currentTask - #https://docs.microsoft.com/en-us/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group + #https://learn.microsoft.com/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2020-02-01" $method = 'GET' $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask @@ -3465,7 +3465,7 @@ function getMDfCSecureScoreMG { $start = Get-Date $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' Write-Host $currentTask - #ref: https://docs.microsoft.com/en-us/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group + #ref: https://learn.microsoft.com/azure/governance/management-groups/resource-graph-samples#secure-score-per-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -3607,7 +3607,7 @@ function getOrphanedResources { $subsToProcessInCustomDataCollection = $using:subsToProcessInCustomDataCollection $azAPICallConf = $using:azAPICallConf - #Batching: https://docs.microsoft.com/en-us/azure/governance/resource-graph/troubleshoot/general#toomanysubscription + #Batching: https://learn.microsoft.com/azure/governance/resource-graph/troubleshoot/general#toomanysubscription $counterBatch = [PSCustomObject] @{ Value = 0 } $batchSize = 1000 $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } @@ -3939,7 +3939,7 @@ function getPolicyHash { function getPolicyRemediation { $currentTask = 'Getting NonCompliant (dine/modify)' Write-Host $currentTask - #ref: https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources + #ref: https://learn.microsoft.com/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -8096,7 +8096,7 @@ function processNetwork { $Mask = $AddressPrefix.substring($AddressPrefix.Length - 2, 2) #Amount of available IP Addresses minus the 3 IPs that Azure consumes, minus net and broadcast - #https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets + #https://learn.microsoft.com/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets switch ($Mask) { '30' { $AvailableAddresses = [Math]::Pow(2, 2) - 5 } '29' { $AvailableAddresses = [Math]::Pow(2, 3) - 5 } @@ -8605,7 +8605,7 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc Subscription Path: $subPath State: $subscriptionState QuotaId: $subscriptionQuotaId - Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs + Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles @@ -8633,18 +8633,18 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +
"@) if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
+    Using deprecated plan 'Container registries' docs
'@) } if ($defenderPlanSubscriptionDeprecatedKubernetesService) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
+    Using deprecated plan 'Kubernetes' docs
'@) } @@ -8746,7 +8746,7 @@ tf.init();}} if ($subscriptionSkippedMDfC.Count -gt 0) { if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs "@) } else { @@ -8758,7 +8758,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs + No Microsoft Defender for Cloud plans docs '@) } } @@ -8900,7 +8900,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs + No Subscription Diagnostic settings docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9022,7 +9022,7 @@ extensions: [{ name: 'sort' }]
-   Resource naming and tagging decision guide docs
+   Resource naming and tagging decision guide docs
   Download CSV semicolon | comma @@ -9096,7 +9096,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs + Tag Name Usage ($tagsUsageCount Tags) docs "@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9380,7 +9380,7 @@ extensions: [{ name: 'sort' }]
-   Set up preview features in Azure subscription docs +   Set up preview features in Azure subscription docs
@@ -9446,7 +9446,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs + 0 enabled Subscription Features docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9473,7 +9473,7 @@ extensions: [{ name: 'sort' }]
-   Considerations before applying locks docs +   Considerations before applying locks docs
@@ -9541,7 +9541,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs + 0 Resource Locks docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9559,7 +9559,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@" - +
$(($mgAllChildMgs).count -1) ManagementGroups below this scope
$(($mgAllChildSubscriptions).count) Subscriptions below this scope
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
"@) @@ -9695,7 +9695,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs + No Management Group Diagnostic settings docs '@) } #endregion ScopeInsightsDiagnosticsMg @@ -10070,7 +10070,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@"
-   CAF - Recommended abbreviations for Azure resource types docs
+   CAF - Recommended abbreviations for Azure resource types docs
   Resource details can be found in the CSV output *_ResourcesAll.csv
   Download CSV semicolon | comma @@ -10621,7 +10621,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
@@ -19617,14 +19617,14 @@ extensions: [{ name: 'sort' }] #region SUMMARYMGdefault Write-Host ' processing TenantSummary ManagementGroups - default Management Group' [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

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

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

+

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

"@) #endregion SUMMARYMGRequireAuthorizationForGroupCreation @@ -19671,8 +19671,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Supported Microsoft Azure offers docs
- Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
+ Supported Microsoft Azure offers docs
+ Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
Download CSV semicolon | comma
@@ -20076,7 +20076,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Resource naming and tagging decision guide docs
+ Resource naming and tagging decision guide docs
Download CSV semicolon | comma
@@ -20151,7 +20151,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

Tag Name Usage ($tagsUsageCount Tags) docs

+

Tag Name Usage ($tagsUsageCount Tags) docs

"@) } #endregion SUMMARYTagNameUsage @@ -20477,7 +20477,7 @@ extensions: [{ name: 'sort' }]
- CAF - Recommended abbreviations for Azure resource types docs
+ CAF - Recommended abbreviations for Azure resource types docs
Resource details can be found in the CSV output *_ResourcesAll.csv
Download CSV semicolon | comma
@@ -21086,7 +21086,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Set up preview features in Azure subscription docs
+ Set up preview features in Azure subscription docs
Download CSV semicolon | comma
@@ -21163,7 +21163,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No enabled Subscriptions Features docs

+

No enabled Subscriptions Features docs

'@) } $endSubFeatures = Get-Date @@ -21190,7 +21190,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Considerations before applying locks docs
+ Considerations before applying locks docs
Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv
@@ -21259,7 +21259,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No Resource Locks at all docs

+

No Resource Locks at all docs

'@) } $endResourceLocks = Get-Date @@ -21279,8 +21279,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Register Resource Provider 'Microsoft.Security' docs
- Microsoft Defender for Cloud's enhanced security features docs
+ Register Resource Provider 'Microsoft.Security' docs
+ Microsoft Defender for Cloud's enhanced security features docs
Download CSV semicolon | comma
@@ -21378,17 +21378,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21460,17 +21460,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21637,7 +21637,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
@@ -23102,7 +23102,7 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { [void]$htmlTenantSummary.AppendLine(@"
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23240,7 +23240,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Management Groups configured for Diagnostic settings docs

+

No Management Groups configured for Diagnostic settings docs

'@) } @@ -23249,9 +23249,9 @@ extensions: [{ name: 'sort' }] $tfCount = $arrayMgsWithoutDiagnosticsCount $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' [void]$htmlTenantSummary.AppendLine(@" - +
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23326,7 +23326,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Management Groups are configured for Diagnostic settings docs

+

All Management Groups are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsManagementGroups @@ -23346,7 +23346,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23480,7 +23480,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Subscriptions configured for Diagnostic settings docs

+

No Subscriptions configured for Diagnostic settings docs

'@) } @@ -23491,7 +23491,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23566,7 +23566,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Subscriptions are configured for Diagnostic settings docs

+

All Subscriptions are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsSubscriptions @@ -23593,7 +23593,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs
+ Supported categories for Azure Resource Logs docs
Download CSV semicolon | comma
@@ -23917,7 +23917,7 @@ extensions: [{ name: 'sort' }] else { $resourceCount = '0' } - $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ Priority = '2-Medium' PolicyId = 'n/a' @@ -23952,7 +23952,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs + Supported categories for Azure Resource Logs docs
@@ -23984,7 +23984,7 @@ extensions: [{ name: 'sort' }] $($diagnosticsFinding.Recommendation)
- $($diagnosticsFinding.ResourceType) + $($diagnosticsFinding.ResourceType) $($diagnosticsFinding.ResourceTypeCount) @@ -24129,24 +24129,24 @@ extensions: [{ name: 'sort' }] #policySets if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { [void]$htmlTenantSummary.AppendLine(@" -

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

"@) } @@ -24166,7 +24166,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma @@ -24239,7 +24239,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyAssignments @@ -24253,7 +24253,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24326,7 +24326,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyScope @@ -24340,7 +24340,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24413,7 +24413,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicySetScope @@ -24428,7 +24428,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -24501,7 +24501,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsRoleAssignment @@ -24522,7 +24522,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Resource Group Limit docs
+ Azure Subscription Resource Group Limit docs
Download CSV semicolon | comma
@@ -24596,7 +24596,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

+ $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

"@) } #endregion SUMMARYSubsapproachingLimitsResourceGroups @@ -24610,7 +24610,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Tag Limit docs
+ Azure Subscription Tag Limit docs
Download CSV semicolon | comma
@@ -24683,7 +24683,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsSubscriptionTags @@ -24697,7 +24697,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24770,7 +24770,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyAssignments @@ -24784,7 +24784,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24857,7 +24857,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyScope @@ -24871,7 +24871,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24944,7 +24944,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicySetScope @@ -24961,7 +24961,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -25034,7 +25034,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

+ $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

"@) } #endregion SUMMARYSubsapproachingLimitsRoleAssignment @@ -25275,7 +25275,7 @@ tf.init();}} } } else { - #https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.escape + #https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.regex.escape $s1 = $altName -replace '.*/providers/' $rm = $s1 -replace '.*/' $resourceType = $s1 -replace "/$([System.Text.RegularExpressions.Regex]::Escape($rm))" @@ -29265,7 +29265,7 @@ function dataCollectionDefenderPlans { ) $currentTask = "Getting Microsoft Defender for Cloud plans for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/pricings?api-version=2018-06-01" $method = 'GET' $defenderPlansResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29350,7 +29350,7 @@ function dataCollectionDefenderEmailContacts { ) $currentTask = "Getting Microsoft Defender for Cloud Email contacts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview" $method = 'GET' $defenderSecurityContactsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -listenOn 'Content' -currentTask $currentTask -caller 'CustomDataCollection' @@ -29447,7 +29447,7 @@ function dataCollectionVNets { ) $currentTask = "Getting Virtual Networks for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/virtualNetworks?api-version=2022-05-01" $method = 'GET' $networkResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29474,7 +29474,7 @@ function dataCollectionPrivateEndpoints { ) $currentTask = "Getting Private Endpoints for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/privateEndpoints?api-version=2022-05-01" $method = 'GET' $privateEndpointsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -unhandledErrorAction Continue @@ -33674,7 +33674,7 @@ function processScopeInsights($mgChild, $mgChildOf) { "@ if ($mgId -eq $defaultManagementGroupId) { $script:html += @' -
+ '@ } $script:html += @" diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index 98dbf86a..e8cfb51c 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -578,14 +578,14 @@ Param [switch] $ShowRunIdentifier, - #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits + #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits [int] $LimitRBACCustomRoleDefinitionsTenant = 5000, [int] $LimitRBACRoleAssignmentsManagementGroup = 500, - #https://docs.microsoft.com/en-us/azure/governance/policy/overview#maximum-count-of-azure-policy-objects + #https://learn.microsoft.com/azure/governance/policy/overview#maximum-count-of-azure-policy-objects [int] $LimitPOLICYPolicyAssignmentsManagementGroup = 200, @@ -613,7 +613,7 @@ Param [int] $LimitPOLICYPolicySetDefinitionsScopedSubscription = 200, - #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits + #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits [int] $LimitResourceGroups = 980, @@ -2592,7 +2592,7 @@ function getConsumption { #$subscriptionIdsOptimizedForBody = '"{0}"' -f ($subsToProcessInCustomDataCollection.subscriptionId -join '","') $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for $($subsToProcessInCustomDataCollectionCount) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" $method = 'POST' @@ -2743,7 +2743,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -2804,7 +2804,7 @@ function getConsumption { #region mgScope $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" $method = 'POST' $body = @" @@ -2935,7 +2935,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -3156,7 +3156,7 @@ function getConsumption { function getDefaultManagementGroup { $currentTask = 'Get Default Management Group' Write-Host $currentTask - #https://docs.microsoft.com/en-us/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group + #https://learn.microsoft.com/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2020-02-01" $method = 'GET' $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask @@ -3465,7 +3465,7 @@ function getMDfCSecureScoreMG { $start = Get-Date $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' Write-Host $currentTask - #ref: https://docs.microsoft.com/en-us/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group + #ref: https://learn.microsoft.com/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -3607,7 +3607,7 @@ function getOrphanedResources { $subsToProcessInCustomDataCollection = $using:subsToProcessInCustomDataCollection $azAPICallConf = $using:azAPICallConf - #Batching: https://docs.microsoft.com/en-us/azure/governance/resource-graph/troubleshoot/general#toomanysubscription + #Batching: https://learn.microsoft.com/azure/governance/resource-graph/troubleshoot/general#toomanysubscription $counterBatch = [PSCustomObject] @{ Value = 0 } $batchSize = 1000 $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } @@ -3939,7 +3939,7 @@ function getPolicyHash { function getPolicyRemediation { $currentTask = 'Getting NonCompliant (dine/modify)' Write-Host $currentTask - #ref: https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources + #ref: https://learn.microsoft.com/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -8096,7 +8096,7 @@ function processNetwork { $Mask = $AddressPrefix.substring($AddressPrefix.Length - 2, 2) #Amount of available IP Addresses minus the 3 IPs that Azure consumes, minus net and broadcast - #https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets + #https://learn.microsoft.com/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets switch ($Mask) { '30' { $AvailableAddresses = [Math]::Pow(2, 2) - 5 } '29' { $AvailableAddresses = [Math]::Pow(2, 3) - 5 } @@ -8605,7 +8605,7 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc - + @@ -8633,18 +8633,18 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +
"@) if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
+    Using deprecated plan 'Container registries' docs
'@) } if ($defenderPlanSubscriptionDeprecatedKubernetesService) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
+    Using deprecated plan 'Kubernetes' docs
'@) } @@ -8746,7 +8746,7 @@ tf.init();}} if ($subscriptionSkippedMDfC.Count -gt 0) { if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs "@) } else { @@ -8758,7 +8758,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs + No Microsoft Defender for Cloud plans docs '@) } } @@ -8900,7 +8900,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs + No Subscription Diagnostic settings docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9022,7 +9022,7 @@ extensions: [{ name: 'sort' }]
-   Resource naming and tagging decision guide docs
+   Resource naming and tagging decision guide docs
   Download CSV semicolon | comma

Default Management Group docs

Default Management Group docs

Subscription Path: $subPath
State: $subscriptionState
QuotaId: $subscriptionQuotaId
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState
Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity
Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles
@@ -9096,7 +9096,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs + Tag Name Usage ($tagsUsageCount Tags) docs "@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9380,7 +9380,7 @@ extensions: [{ name: 'sort' }]
-   Set up preview features in Azure subscription docs +   Set up preview features in Azure subscription docs
@@ -9446,7 +9446,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs + 0 enabled Subscription Features docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9473,7 +9473,7 @@ extensions: [{ name: 'sort' }]
-   Considerations before applying locks docs +   Considerations before applying locks docs
@@ -9541,7 +9541,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs + 0 Resource Locks docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9559,7 +9559,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@" - +
$(($mgAllChildMgs).count -1) ManagementGroups below this scope
$(($mgAllChildSubscriptions).count) Subscriptions below this scope
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
"@) @@ -9695,7 +9695,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs + No Management Group Diagnostic settings docs '@) } #endregion ScopeInsightsDiagnosticsMg @@ -10070,7 +10070,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@"
-   CAF - Recommended abbreviations for Azure resource types docs
+   CAF - Recommended abbreviations for Azure resource types docs
   Resource details can be found in the CSV output *_ResourcesAll.csv
   Download CSV semicolon | comma @@ -10621,7 +10621,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
@@ -19618,14 +19618,14 @@ extensions: [{ name: 'sort' }] #region SUMMARYMGdefault Write-Host ' processing TenantSummary ManagementGroups - default Management Group' [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

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

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

+

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

"@) #endregion SUMMARYMGRequireAuthorizationForGroupCreation @@ -19672,8 +19672,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Supported Microsoft Azure offers docs
- Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
+ Supported Microsoft Azure offers docs
+ Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
Download CSV semicolon | comma
@@ -20077,7 +20077,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Resource naming and tagging decision guide docs
+ Resource naming and tagging decision guide docs
Download CSV semicolon | comma
@@ -20152,7 +20152,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

Tag Name Usage ($tagsUsageCount Tags) docs

+

Tag Name Usage ($tagsUsageCount Tags) docs

"@) } #endregion SUMMARYTagNameUsage @@ -20478,7 +20478,7 @@ extensions: [{ name: 'sort' }]
- CAF - Recommended abbreviations for Azure resource types docs
+ CAF - Recommended abbreviations for Azure resource types docs
Resource details can be found in the CSV output *_ResourcesAll.csv
Download CSV semicolon | comma
@@ -21087,7 +21087,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Set up preview features in Azure subscription docs
+ Set up preview features in Azure subscription docs
Download CSV semicolon | comma
@@ -21164,7 +21164,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No enabled Subscriptions Features docs

+

No enabled Subscriptions Features docs

'@) } $endSubFeatures = Get-Date @@ -21191,7 +21191,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Considerations before applying locks docs
+ Considerations before applying locks docs
Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv
@@ -21260,7 +21260,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No Resource Locks at all docs

+

No Resource Locks at all docs

'@) } $endResourceLocks = Get-Date @@ -21280,8 +21280,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Register Resource Provider 'Microsoft.Security' docs
- Microsoft Defender for Cloud's enhanced security features docs
+ Register Resource Provider 'Microsoft.Security' docs
+ Microsoft Defender for Cloud's enhanced security features docs
Download CSV semicolon | comma
@@ -21379,17 +21379,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21461,17 +21461,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21638,7 +21638,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
@@ -23103,7 +23103,7 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { [void]$htmlTenantSummary.AppendLine(@"
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23241,7 +23241,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Management Groups configured for Diagnostic settings docs

+

No Management Groups configured for Diagnostic settings docs

'@) } @@ -23250,9 +23250,9 @@ extensions: [{ name: 'sort' }] $tfCount = $arrayMgsWithoutDiagnosticsCount $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' [void]$htmlTenantSummary.AppendLine(@" - +
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23327,7 +23327,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Management Groups are configured for Diagnostic settings docs

+

All Management Groups are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsManagementGroups @@ -23347,7 +23347,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23481,7 +23481,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Subscriptions configured for Diagnostic settings docs

+

No Subscriptions configured for Diagnostic settings docs

'@) } @@ -23492,7 +23492,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23567,7 +23567,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Subscriptions are configured for Diagnostic settings docs

+

All Subscriptions are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsSubscriptions @@ -23594,7 +23594,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs
+ Supported categories for Azure Resource Logs docs
Download CSV semicolon | comma
@@ -23918,7 +23918,7 @@ extensions: [{ name: 'sort' }] else { $resourceCount = '0' } - $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ Priority = '2-Medium' PolicyId = 'n/a' @@ -23953,7 +23953,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs + Supported categories for Azure Resource Logs docs
@@ -23985,7 +23985,7 @@ extensions: [{ name: 'sort' }] $($diagnosticsFinding.Recommendation)
- $($diagnosticsFinding.ResourceType) + $($diagnosticsFinding.ResourceType) $($diagnosticsFinding.ResourceTypeCount) @@ -24130,24 +24130,24 @@ extensions: [{ name: 'sort' }] #policySets if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { [void]$htmlTenantSummary.AppendLine(@" -

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

"@) } @@ -24167,7 +24167,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma @@ -24240,7 +24240,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyAssignments @@ -24254,7 +24254,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24327,7 +24327,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyScope @@ -24341,7 +24341,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24414,7 +24414,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicySetScope @@ -24429,7 +24429,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -24502,7 +24502,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsRoleAssignment @@ -24523,7 +24523,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Resource Group Limit docs
+ Azure Subscription Resource Group Limit docs
Download CSV semicolon | comma
@@ -24597,7 +24597,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

+ $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

"@) } #endregion SUMMARYSubsapproachingLimitsResourceGroups @@ -24611,7 +24611,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Tag Limit docs
+ Azure Subscription Tag Limit docs
Download CSV semicolon | comma
@@ -24684,7 +24684,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsSubscriptionTags @@ -24698,7 +24698,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24771,7 +24771,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyAssignments @@ -24785,7 +24785,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24858,7 +24858,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyScope @@ -24872,7 +24872,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24945,7 +24945,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicySetScope @@ -24962,7 +24962,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -25035,7 +25035,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

+ $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

"@) } #endregion SUMMARYSubsapproachingLimitsRoleAssignment @@ -29263,7 +29263,7 @@ function dataCollectionDefenderPlans { ) $currentTask = "Getting Microsoft Defender for Cloud plans for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/pricings?api-version=2018-06-01" $method = 'GET' $defenderPlansResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29348,7 +29348,7 @@ function dataCollectionDefenderEmailContacts { ) $currentTask = "Getting Microsoft Defender for Cloud Email contacts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview" $method = 'GET' $defenderSecurityContactsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -listenOn 'Content' -currentTask $currentTask -caller 'CustomDataCollection' @@ -29445,7 +29445,7 @@ function dataCollectionVNets { ) $currentTask = "Getting Virtual Networks for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/virtualNetworks?api-version=2022-05-01" $method = 'GET' $networkResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29472,7 +29472,7 @@ function dataCollectionPrivateEndpoints { ) $currentTask = "Getting Private Endpoints for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/privateEndpoints?api-version=2022-05-01" $method = 'GET' $privateEndpointsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -unhandledErrorAction Continue @@ -33672,7 +33672,7 @@ function processScopeInsights($mgChild, $mgChildOf) { "@ if ($mgId -eq $defaultManagementGroupId) { $script:html += @' -
+ '@ } $script:html += @" diff --git a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 index f2f5a5fd..4a789115 100644 --- a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 +++ b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 @@ -27,7 +27,7 @@ function dataCollectionDefenderPlans { ) $currentTask = "Getting Microsoft Defender for Cloud plans for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/defenderforcloud/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/pricings?api-version=2018-06-01" $method = 'GET' $defenderPlansResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -112,7 +112,7 @@ function dataCollectionDefenderEmailContacts { ) $currentTask = "Getting Microsoft Defender for Cloud Email contacts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/defenderforcloud/security-contacts $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview" $method = 'GET' $defenderSecurityContactsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -listenOn 'Content' -currentTask $currentTask -caller 'CustomDataCollection' @@ -209,7 +209,6 @@ function dataCollectionVNets { ) $currentTask = "Getting Virtual Networks for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/virtualNetworks?api-version=2022-05-01" $method = 'GET' $networkResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -236,7 +235,6 @@ function dataCollectionPrivateEndpoints { ) $currentTask = "Getting Private Endpoints for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/privateEndpoints?api-version=2022-05-01" $method = 'GET' $privateEndpointsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -unhandledErrorAction Continue diff --git a/pwsh/dev/functions/getConsumption.ps1 b/pwsh/dev/functions/getConsumption.ps1 index 95830d8e..9c502f97 100644 --- a/pwsh/dev/functions/getConsumption.ps1 +++ b/pwsh/dev/functions/getConsumption.ps1 @@ -33,7 +33,7 @@ function getConsumption { #$subscriptionIdsOptimizedForBody = '"{0}"' -f ($subsToProcessInCustomDataCollection.subscriptionId -join '","') $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for $($subsToProcessInCustomDataCollectionCount) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' @@ -184,7 +184,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -245,7 +245,7 @@ function getConsumption { #region mgScope $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $body = @" @@ -376,7 +376,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' diff --git a/pwsh/dev/functions/getDefaultManagementGroup.ps1 b/pwsh/dev/functions/getDefaultManagementGroup.ps1 index 7b77701b..61193635 100644 --- a/pwsh/dev/functions/getDefaultManagementGroup.ps1 +++ b/pwsh/dev/functions/getDefaultManagementGroup.ps1 @@ -1,7 +1,7 @@ function getDefaultManagementGroup { $currentTask = 'Get Default Management Group' Write-Host $currentTask - #https://docs.microsoft.com/en-us/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group + #https://learn.microsoft.com/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2020-02-01" $method = 'GET' $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask diff --git a/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 b/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 index c72c382f..c903f3fc 100644 --- a/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 +++ b/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 @@ -2,7 +2,7 @@ function getMDfCSecureScoreMG { $start = Get-Date $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' Write-Host $currentTask - #ref: https://docs.microsoft.com/en-us/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group + #ref: https://learn.microsoft.com/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' diff --git a/pwsh/dev/functions/getOrphanedResources.ps1 b/pwsh/dev/functions/getOrphanedResources.ps1 index 346aac3f..cdf2a3cf 100644 --- a/pwsh/dev/functions/getOrphanedResources.ps1 +++ b/pwsh/dev/functions/getOrphanedResources.ps1 @@ -86,7 +86,7 @@ function getOrphanedResources { $subsToProcessInCustomDataCollection = $using:subsToProcessInCustomDataCollection $azAPICallConf = $using:azAPICallConf - #Batching: https://docs.microsoft.com/en-us/azure/governance/resource-graph/troubleshoot/general#toomanysubscription + #Batching: https://learn.microsoft.com/azure/governance/resource-graph/troubleshoot/general#toomanysubscription $counterBatch = [PSCustomObject] @{ Value = 0 } $batchSize = 1000 $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } diff --git a/pwsh/dev/functions/getPolicyRemediation.ps1 b/pwsh/dev/functions/getPolicyRemediation.ps1 index ebad7d98..d9bbc13a 100644 --- a/pwsh/dev/functions/getPolicyRemediation.ps1 +++ b/pwsh/dev/functions/getPolicyRemediation.ps1 @@ -1,7 +1,7 @@ function getPolicyRemediation { $currentTask = 'Getting NonCompliant (dine/modify)' Write-Host $currentTask - #ref: https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources + #ref: https://learn.microsoft.com/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' diff --git a/pwsh/dev/functions/html/htmlFunctions.ps1 b/pwsh/dev/functions/html/htmlFunctions.ps1 index 8893e8a0..a30810c8 100644 --- a/pwsh/dev/functions/html/htmlFunctions.ps1 +++ b/pwsh/dev/functions/html/htmlFunctions.ps1 @@ -238,7 +238,7 @@ function processScopeInsights($mgChild, $mgChildOf) { "@ if ($mgId -eq $defaultManagementGroupId) { $script:html += @' - + '@ } $script:html += @" diff --git a/pwsh/dev/functions/processNetwork.ps1 b/pwsh/dev/functions/processNetwork.ps1 index a24ad516..8ca07356 100644 --- a/pwsh/dev/functions/processNetwork.ps1 +++ b/pwsh/dev/functions/processNetwork.ps1 @@ -378,7 +378,7 @@ function processNetwork { $Mask = $AddressPrefix.substring($AddressPrefix.Length - 2, 2) #Amount of available IP Addresses minus the 3 IPs that Azure consumes, minus net and broadcast - #https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets + #https://learn.microsoft.com/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets switch ($Mask) { '30' { $AvailableAddresses = [Math]::Pow(2, 2) - 5 } '29' { $AvailableAddresses = [Math]::Pow(2, 3) - 5 } diff --git a/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 b/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 index 9ed6cc45..14caed8b 100644 --- a/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 +++ b/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 @@ -145,7 +145,7 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc - + @@ -173,18 +173,18 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +
"@) if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
+    Using deprecated plan 'Container registries' docs
'@) } if ($defenderPlanSubscriptionDeprecatedKubernetesService) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
+    Using deprecated plan 'Kubernetes' docs
'@) } @@ -286,7 +286,7 @@ tf.init();}} if ($subscriptionSkippedMDfC.Count -gt 0) { if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs "@) } else { @@ -298,7 +298,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs + No Microsoft Defender for Cloud plans docs '@) } } @@ -440,7 +440,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs + No Subscription Diagnostic settings docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -562,7 +562,7 @@ extensions: [{ name: 'sort' }]
-   Resource naming and tagging decision guide docs
+   Resource naming and tagging decision guide docs
   Download CSV semicolon | comma

Default Management Group docs

Default Management Group docs

Default Management Group docs

Default Management Group docs

Subscription Path: $subPath
State: $subscriptionState
QuotaId: $subscriptionQuotaId
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState
Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity
Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles
@@ -636,7 +636,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs + Tag Name Usage ($tagsUsageCount Tags) docs "@) } [void]$htmlScopeInsights.AppendLine(@' @@ -920,7 +920,7 @@ extensions: [{ name: 'sort' }]
-   Set up preview features in Azure subscription docs +   Set up preview features in Azure subscription docs
@@ -986,7 +986,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs + 0 enabled Subscription Features docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -1013,7 +1013,7 @@ extensions: [{ name: 'sort' }]
-   Considerations before applying locks docs +   Considerations before applying locks docs
@@ -1081,7 +1081,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs + 0 Resource Locks docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -1099,7 +1099,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@" - +
$(($mgAllChildMgs).count -1) ManagementGroups below this scope
$(($mgAllChildSubscriptions).count) Subscriptions below this scope
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
"@) @@ -1235,7 +1235,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs + No Management Group Diagnostic settings docs '@) } #endregion ScopeInsightsDiagnosticsMg @@ -1610,7 +1610,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@"
-   CAF - Recommended abbreviations for Azure resource types docs
+   CAF - Recommended abbreviations for Azure resource types docs
   Resource details can be found in the CSV output *_ResourcesAll.csv
   Download CSV semicolon | comma @@ -2161,7 +2161,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
diff --git a/pwsh/dev/functions/processTenantSummary.ps1 b/pwsh/dev/functions/processTenantSummary.ps1 index 2f873437..1cc84c62 100644 --- a/pwsh/dev/functions/processTenantSummary.ps1 +++ b/pwsh/dev/functions/processTenantSummary.ps1 @@ -6860,14 +6860,14 @@ extensions: [{ name: 'sort' }] #region SUMMARYMGdefault Write-Host ' processing TenantSummary ManagementGroups - default Management Group' [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

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

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

+

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

"@) #endregion SUMMARYMGRequireAuthorizationForGroupCreation @@ -6914,8 +6914,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Supported Microsoft Azure offers docs
- Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
+ Supported Microsoft Azure offers docs
+ Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
Download CSV semicolon | comma
@@ -7319,7 +7319,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Resource naming and tagging decision guide docs
+ Resource naming and tagging decision guide docs
Download CSV semicolon | comma
@@ -7394,7 +7394,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

Tag Name Usage ($tagsUsageCount Tags) docs

+

Tag Name Usage ($tagsUsageCount Tags) docs

"@) } #endregion SUMMARYTagNameUsage @@ -7720,7 +7720,7 @@ extensions: [{ name: 'sort' }]
- CAF - Recommended abbreviations for Azure resource types docs
+ CAF - Recommended abbreviations for Azure resource types docs
Resource details can be found in the CSV output *_ResourcesAll.csv
Download CSV semicolon | comma
@@ -8329,7 +8329,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Set up preview features in Azure subscription docs
+ Set up preview features in Azure subscription docs
Download CSV semicolon | comma
@@ -8406,7 +8406,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No enabled Subscriptions Features docs

+

No enabled Subscriptions Features docs

'@) } $endSubFeatures = Get-Date @@ -8433,7 +8433,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Considerations before applying locks docs
+ Considerations before applying locks docs
Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv
@@ -8502,7 +8502,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No Resource Locks at all docs

+

No Resource Locks at all docs

'@) } $endResourceLocks = Get-Date @@ -8522,8 +8522,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Register Resource Provider 'Microsoft.Security' docs
- Microsoft Defender for Cloud's enhanced security features docs
+ Register Resource Provider 'Microsoft.Security' docs
+ Microsoft Defender for Cloud's enhanced security features docs
Download CSV semicolon | comma
@@ -8621,17 +8621,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -8703,17 +8703,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -8880,7 +8880,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
@@ -10345,7 +10345,7 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { [void]$htmlTenantSummary.AppendLine(@"
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -10483,7 +10483,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Management Groups configured for Diagnostic settings docs

+

No Management Groups configured for Diagnostic settings docs

'@) } @@ -10492,9 +10492,9 @@ extensions: [{ name: 'sort' }] $tfCount = $arrayMgsWithoutDiagnosticsCount $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' [void]$htmlTenantSummary.AppendLine(@" - +
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -10569,7 +10569,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Management Groups are configured for Diagnostic settings docs

+

All Management Groups are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsManagementGroups @@ -10589,7 +10589,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -10723,7 +10723,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Subscriptions configured for Diagnostic settings docs

+

No Subscriptions configured for Diagnostic settings docs

'@) } @@ -10734,7 +10734,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -10809,7 +10809,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Subscriptions are configured for Diagnostic settings docs

+

All Subscriptions are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsSubscriptions @@ -10836,7 +10836,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs
+ Supported categories for Azure Resource Logs docs
Download CSV semicolon | comma
@@ -11160,7 +11160,7 @@ extensions: [{ name: 'sort' }] else { $resourceCount = '0' } - $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ Priority = '2-Medium' PolicyId = 'n/a' @@ -11195,7 +11195,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs + Supported categories for Azure Resource Logs docs
@@ -11227,7 +11227,7 @@ extensions: [{ name: 'sort' }] $($diagnosticsFinding.Recommendation)
- $($diagnosticsFinding.ResourceType) + $($diagnosticsFinding.ResourceType) $($diagnosticsFinding.ResourceTypeCount) @@ -11372,24 +11372,24 @@ extensions: [{ name: 'sort' }] #policySets if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { [void]$htmlTenantSummary.AppendLine(@" -

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

"@) } @@ -11409,7 +11409,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma @@ -11482,7 +11482,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyAssignments @@ -11496,7 +11496,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -11569,7 +11569,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyScope @@ -11583,7 +11583,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -11656,7 +11656,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicySetScope @@ -11671,7 +11671,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -11744,7 +11744,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsRoleAssignment @@ -11765,7 +11765,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Resource Group Limit docs
+ Azure Subscription Resource Group Limit docs
Download CSV semicolon | comma
@@ -11839,7 +11839,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

+ $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

"@) } #endregion SUMMARYSubsapproachingLimitsResourceGroups @@ -11853,7 +11853,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Tag Limit docs
+ Azure Subscription Tag Limit docs
Download CSV semicolon | comma
@@ -11926,7 +11926,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsSubscriptionTags @@ -11940,7 +11940,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -12013,7 +12013,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyAssignments @@ -12027,7 +12027,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -12100,7 +12100,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyScope @@ -12114,7 +12114,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -12187,7 +12187,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicySetScope @@ -12204,7 +12204,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -12277,7 +12277,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

+ $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

"@) } #endregion SUMMARYSubsapproachingLimitsRoleAssignment @@ -12518,7 +12518,7 @@ tf.init();}} } } else { - #https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.escape + #https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.regex.escape $s1 = $altName -replace '.*/providers/' $rm = $s1 -replace '.*/' $resourceType = $s1 -replace "/$([System.Text.RegularExpressions.Regex]::Escape($rm))" diff --git a/run-from/console.md b/run-from/console.md index d55c4d1d..a0cd9e83 100644 --- a/run-from/console.md +++ b/run-from/console.md @@ -30,7 +30,7 @@ _- or -_ ### Set up to execute as a tenant _guest user_ -Your user is a [guest user](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#compare-member-and-guest-default-permissions) in the tenant or there are other [hardened restrictions](https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions) on the tenant, then your user must first be assigned the Microsoft Entra ID role '**Directory readers**'. Work with the Microsoft Entra administrator for the tenant you are a guest in to have them assign the '**Directory readers**' [role to your guest account](https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal). +Your user is a [guest user](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#compare-member-and-guest-default-permissions) in the tenant or there are other [hardened restrictions](https://learn.microsoft.com/entra/identity/users/users-restrict-guest-permissions) on the tenant, then your user must first be assigned the Microsoft Entra ID role '**Directory readers**'. Work with the Microsoft Entra administrator for the tenant you are a guest in to have them assign the '**Directory readers**' [role to your guest account](https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal). :arrow_down_small: Once that is configured, continue with [**2. Validate Azure permissions for your user**](#2-validate-azure-permissions-for-your-user). diff --git a/setup.md b/setup.md index 427769cc..ff60d736 100644 --- a/setup.md +++ b/setup.md @@ -11,7 +11,7 @@ No matter which of the three you choose, they all evaluate the same governance c ## Prerequisites - Your user must have '**Microsoft.Authorization/roleAssignments/write**' permissions on the target management group scope (such as the built-in Azure RBAC role '**User Access Administrator**' or '**Owner**'). This is required to make the required permission changes. If you cannot do this yourself, follow these instructions along with someone who can. -- To grant Microsoft Graph API permissions and grant admin consent for the Microsoft Entra directory, you must yourself have or work with someone that has the '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. (See [Assign Microsoft Entra roles to users](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal).) +- To grant Microsoft Graph API permissions and grant admin consent for the Microsoft Entra directory, you must yourself have or work with someone that has the '**Privileged Role Administrator**' or '**Global Administrator**' role assigned in Microsoft Entra ID. (See [Assign Microsoft Entra roles to users](https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal).) ## Set up and run Azure Governance Visualizer from the console From 7b8b3b13e72b11f973d4f2ae545ca451647b0bfc Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 10 Jan 2024 14:43:17 +0000 Subject: [PATCH 10/18] More Entra rebranding --- .azuredevops/pipelines/AzGovViz.variables.yml | 2 +- README.md | 6 +-- pwsh/AzGovVizParallel.ps1 | 42 +++++++++--------- pwsh/dev/devAzGovVizParallel.ps1 | 44 +++++++++---------- pwsh/dev/functions/processAADGroups.ps1 | 22 +++++----- .../functions/processScopeInsightsMgOrSub.ps1 | 2 +- pwsh/dev/functions/processTenantSummary.ps1 | 6 +-- pwsh/dev/functions/runInfo.ps1 | 4 +- 8 files changed, 64 insertions(+), 64 deletions(-) diff --git a/.azuredevops/pipelines/AzGovViz.variables.yml b/.azuredevops/pipelines/AzGovViz.variables.yml index 415dc88d..c79ef3bf 100644 --- a/.azuredevops/pipelines/AzGovViz.variables.yml +++ b/.azuredevops/pipelines/AzGovViz.variables.yml @@ -207,7 +207,7 @@ variables: # Switch | example: value: true value: - # Will not resolve Microsoft Entra ID (AAD) Group memberships for Role assignments where identity type is 'Group' + # Will not resolve Microsoft Entra group memberships for role assignments where identity type is 'Group' - name: NoAADGroupsResolveMembers # Switch | example: value: true value: diff --git a/README.md b/README.md index 96b60d39..c201e0eb 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,7 @@ These permissions are __mandatory__ in each and every scenario!
- @@ -513,7 +513,7 @@ Screenshot of Microsoft Graph permissions in the Microsoft Entra admin center * `-JsonExportExcludeResources`- JSON Export will not include Resources (Role assignments) * `-LargeTenant` - A large tenant is a tenant with more than ~500 Subscriptions - the HTML output for large tenants simply becomes too big. Using this parameter the following parameters will be set: `-PolicyAtScopeOnly`, `-RBACAtScopeOnly`, `-NoResourceProvidersAtAll`, `-NoScopeInsights` * `-HtmlTableRowsLimit` - Although the parameter `-LargeTenant` was introduced recently, still the html output may become too large to be processed properly. The new parameter defines the limit of rows - if for the html processing part the limit is reached then the html table will not be created (csv and json output will still be created). Default rows limit is 20.000 -* `-AADGroupMembersLimit` - Defines the limit (default=500) of Microsoft Entra ID (AAD) Group members; For Microsoft Entra ID (AAD) Groups that have more members than the defined limit Group members will not be resolved +* `-AADGroupMembersLimit` - Defines the limit (default=500) of Microsoft Entra group members; For Microsoft Entra ID groups that have more members than the defined limit group members will not be resolved * `-NoResources` - Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) * `-StatsOptOut` - Opt out sending [stats](#stats) * `-NoSingleSubscriptionOutput` - Single __Scope Insights__ output per Subscription should not be created @@ -545,7 +545,7 @@ Azure Governance Visualizer polls the following APIs | Endpoint | API version | API name | | -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -| MS Graph | beta | /groups/`aadGroupId`/transitiveMembers | +| MS Graph | beta | /groups/`entraGroupId`/transitiveMembers | | MS Graph | beta | /privilegedAccess/azureResources/resources | | MS Graph | beta | /privilegedAccess/azureResources/roleAssignments | | MS Graph | v1.0 | /applications | diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index cf6dce8a..3d4275e1 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -127,7 +127,7 @@ Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size) .PARAMETER AADGroupMembersLimit - Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved + Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolveds .PARAMETER NoResources Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -302,7 +302,7 @@ Note if you use parameter -LargeTenant then parameter -NoScopeInsights will be set to true PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoScopeInsights - Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved + Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolved PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -AADGroupMembersLimit 750 Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -4287,13 +4287,13 @@ function prepareData { } function processAADGroups { if ($NoPIMEligibility) { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment exists)' } else { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment or PIM Eligibility exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment or PIM eligibility exists)' } - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving AAD Groups)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before resolving Microsoft Entra groups)" $startAADGroupsResolveMembers = Get-Date $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique @@ -4305,7 +4305,7 @@ function processAADGroups { } $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount Groups from RoleAssignments" + Write-Host " $aadGroupsCount groups from role assignments" if (-not $NoPIMEligibility) { $PIMEligibleGroups = $arrayPIMEligible.where({ $_.IdentityType -eq 'Group' }) | Select-Object IdentityObjectId, IdentityDisplayName | Sort-Object -Property IdentityObjectId -Unique @@ -4321,9 +4321,9 @@ function processAADGroups { }) } } - Write-Host " $cntPIMEligibleGroupsTotal Groups from PIM Eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments Groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in RoleAssignments)" + Write-Host " $cntPIMEligibleGroupsTotal groups from PIM eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in role assignments)" $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount Groups from RoleAssignments and PIM Eligibility" + Write-Host " $aadGroupsCount Groups from role assignments and PIM eligibility" } if ($aadGroupsCount -gt 0) { @@ -4339,7 +4339,7 @@ function processAADGroups { { $_ -gt 10000 } { $indicator = 250 } } - Write-Host " processing $($aadGroupsCount) AAD Groups (indicating progress in steps of $indicator)" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { $aadGroupIdWithRoleAssignment = $_ @@ -4392,24 +4392,24 @@ function processAADGroups { $processedAADGroupsCount = ($arrayProgressedAADGroups).Count if ($processedAADGroupsCount) { if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount AAD Groups processed" + Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" } } } -ThrottleLimit ($ThrottleLimit * 2) } else { - Write-Host " processing $($aadGroupsCount) AAD Groups" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" } $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count if ($arrayGroupRequestResourceNotFoundCount -gt 0) { - Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" + Write-Host "$arrayGroupRequestResourceNotFoundCount groups could not be checked for memberships" } - Write-Host " processed $($arrayProgressedAADGroups.Count) AAD Groups" + Write-Host " processed $($arrayProgressedAADGroups.Count) Microsoft Entra groups" $endAADGroupsResolveMembers = Get-Date - Write-Host "Resolving AAD Groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving AAD Groups)" + Write-Host "Resolving Microsoft Entra groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving Microsoft Entra groups)" } function processALZPolicyVersionChecker { $start = Get-Date @@ -10621,7 +10621,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
B
Console | Guest user account
If the tenant is hardened (Microsoft Entra ID (AAD) External Identities / Guest user access = most restrictive) then Guest User must be assigned the Microsoft Entra ID (AAD) Role 'Directory readers'
+
If the tenant is hardened (Microsoft Entra ID External Identities / Guest user access = most restrictive) then Guest User must be assigned the Microsoft Entra role 'Directory readers'
💡 Compare member and guest default permissions
💡 Restrict guest access permissions in Microsoft Entra ID
@@ -18990,7 +18990,7 @@ extensions: [{ name: 'sort' }] $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = $null $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = foreach ($highPrivilegedGuestUserRoleAssignment in ($highPrivilegedGuestUserRoleAssignments)) { if ($highPrivilededGuestUserRoleAssignment.AssignmentType -eq 'indirect') { - $assignmentInfo = "indirect / AAD Group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" + $assignmentInfo = "indirect / Microsoft Entra group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" } else { $assignmentInfo = 'direct' @@ -19667,7 +19667,7 @@ extensions: [{ name: 'sort' }] $tfCount = $summarySubscriptionsCount $htmlTableId = 'TenantSummary_subs' - $abbr = " " + $abbr = " " [void]$htmlTenantSummary.AppendLine(@"
@@ -21637,7 +21637,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
@@ -28143,7 +28143,7 @@ function runInfo { } if (-not $NoAADGroupsResolveMembers) { - Write-Host " AAD Groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving AAD Group memberships" -ForegroundColor Yellow + Write-Host " Microsoft Entra groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving group memberships" -ForegroundColor Yellow $script:paramsUsed += 'NoAADGroupsResolveMembers: false ' if ($AADGroupMembersLimit -eq 500) { Write-Host " AADGroupMembersLimit = $AADGroupMembersLimit" -ForegroundColor Yellow @@ -28155,7 +28155,7 @@ function runInfo { } } else { - Write-Host " AAD Groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green + Write-Host " Microsoft Entra groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green $script:paramsUsed += 'NoAADGroupsResolveMembers: true ' } diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index e8cfb51c..be63985a 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -127,7 +127,7 @@ Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size) .PARAMETER AADGroupMembersLimit - Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved + Defines the limit (default=500) of Microsoft Entra group members; For Microsoft Entra groups that have more members than the defined limit group members will not be resolved .PARAMETER NoResources Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -302,7 +302,7 @@ Note if you use parameter -LargeTenant then parameter -NoScopeInsights will be set to true PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoScopeInsights - Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved + Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolved PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -AADGroupMembersLimit 750 Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -4287,13 +4287,13 @@ function prepareData { } function processAADGroups { if ($NoPIMEligibility) { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment exists)' } else { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment or PIM Eligibility exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment or PIM eligibility exists)' } - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving AAD Groups)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving Microsoft Entra groups)" $startAADGroupsResolveMembers = Get-Date $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique @@ -4305,7 +4305,7 @@ function processAADGroups { } $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount Groups from RoleAssignments" + Write-Host " $aadGroupsCount groups from role assignments" if (-not $NoPIMEligibility) { $PIMEligibleGroups = $arrayPIMEligible.where({ $_.IdentityType -eq 'Group' }) | Select-Object IdentityObjectId, IdentityDisplayName | Sort-Object -Property IdentityObjectId -Unique @@ -4321,9 +4321,9 @@ function processAADGroups { }) } } - Write-Host " $cntPIMEligibleGroupsTotal Groups from PIM Eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments Groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in RoleAssignments)" + Write-Host " $cntPIMEligibleGroupsTotal groups from PIM eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in role assignments)" $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount Groups from RoleAssignments and PIM Eligibility" + Write-Host " $aadGroupsCount groups from role assignments and PIM eligibility" } if ($aadGroupsCount -gt 0) { @@ -4339,7 +4339,7 @@ function processAADGroups { { $_ -gt 10000 } { $indicator = 250 } } - Write-Host " processing $($aadGroupsCount) AAD Groups (indicating progress in steps of $indicator)" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { $aadGroupIdWithRoleAssignment = $_ @@ -4392,13 +4392,13 @@ function processAADGroups { $processedAADGroupsCount = ($arrayProgressedAADGroups).Count if ($processedAADGroupsCount) { if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount AAD Groups processed" + Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" } } } -ThrottleLimit ($ThrottleLimit * 2) } else { - Write-Host " processing $($aadGroupsCount) AAD Groups" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" } $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count @@ -4406,10 +4406,10 @@ function processAADGroups { Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" } - Write-Host " processed $($arrayProgressedAADGroups.Count) AAD Groups" + Write-Host " processed $($arrayProgressedAADGroups.Count) Microsoft Entra groups" $endAADGroupsResolveMembers = Get-Date - Write-Host "Resolving AAD Groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving AAD Groups)" + Write-Host "Resolving Microsoft Entra groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after resolving Microsoft Entra groups)" } function processALZPolicyVersionChecker { $start = Get-Date @@ -10621,7 +10621,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
@@ -18991,7 +18991,7 @@ extensions: [{ name: 'sort' }] $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = $null $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = foreach ($highPrivilegedGuestUserRoleAssignment in ($highPrivilegedGuestUserRoleAssignments)) { if ($highPrivilededGuestUserRoleAssignment.AssignmentType -eq 'indirect') { - $assignmentInfo = "indirect / AAD Group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" + $assignmentInfo = "indirect / Microsoft Entra group membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" } else { $assignmentInfo = 'direct' @@ -19668,7 +19668,7 @@ extensions: [{ name: 'sort' }] $tfCount = $summarySubscriptionsCount $htmlTableId = 'TenantSummary_subs' - $abbr = " " + $abbr = " " [void]$htmlTenantSummary.AppendLine(@"
@@ -21638,7 +21638,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
@@ -25059,7 +25059,7 @@ extensions: [{ name: 'sort' }] '@) #region AADSPNotFound - Write-Host ' processing TenantSummary AAD ServicePrincipals - not found' + Write-Host ' processing TenantSummary Microsoft Entra ServicePrincipals - not found' if ($servicePrincipalRequestResourceNotFoundCount -gt 0) { $tfCount = $servicePrincipalRequestResourceNotFoundCount @@ -28141,7 +28141,7 @@ function runInfo { } if (-not $NoAADGroupsResolveMembers) { - Write-Host " AAD Groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving AAD Group memberships" -ForegroundColor Yellow + Write-Host " Microsoft Entra groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving group memberships" -ForegroundColor Yellow $script:paramsUsed += 'NoAADGroupsResolveMembers: false ' if ($AADGroupMembersLimit -eq 500) { Write-Host " AADGroupMembersLimit = $AADGroupMembersLimit" -ForegroundColor Yellow @@ -28153,7 +28153,7 @@ function runInfo { } } else { - Write-Host " AAD Groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green + Write-Host " Microsoft Entra groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green $script:paramsUsed += 'NoAADGroupsResolveMembers: true ' } @@ -28964,7 +28964,7 @@ function validateAccess { $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/resources?`$select=id,displayName,type,externalId" + $uriExt $res = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -currentTask $currentTask -validateAccess if ($res -eq 'failed') { - $permissionCheckResults += "MSGraph API 'PrivilegedAccess.Read.AzureResources' permission - check FAILED - if you cannot grant this permission or you do not have an AAD Premium 2 license then use parameter -NoPIMEligibility" + $permissionCheckResults += "MSGraph API 'PrivilegedAccess.Read.AzureResources' permission - check FAILED - if you cannot grant this permission or you do not have a Microsoft Entra ID P2 license then use parameter -NoPIMEligibility" $permissionsCheckFailed = $true } else { diff --git a/pwsh/dev/functions/processAADGroups.ps1 b/pwsh/dev/functions/processAADGroups.ps1 index 56e9247b..587d7462 100644 --- a/pwsh/dev/functions/processAADGroups.ps1 +++ b/pwsh/dev/functions/processAADGroups.ps1 @@ -1,12 +1,12 @@ function processAADGroups { if ($NoPIMEligibility) { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment exists)' } else { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment or PIM Eligibility exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment or PIM eligibility exists)' } - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving AAD Groups)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before resolving Microsoft Entra groups)" $startAADGroupsResolveMembers = Get-Date $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique @@ -34,9 +34,9 @@ function processAADGroups { }) } } - Write-Host " $cntPIMEligibleGroupsTotal Groups from PIM Eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments Groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in RoleAssignments)" + Write-Host " $cntPIMEligibleGroupsTotal groups from PIM eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in role assignments)" $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount Groups from RoleAssignments and PIM Eligibility" + Write-Host " $aadGroupsCount groups from role assignments and PIM eligibility" } if ($aadGroupsCount -gt 0) { @@ -52,7 +52,7 @@ function processAADGroups { { $_ -gt 10000 } { $indicator = 250 } } - Write-Host " processing $($aadGroupsCount) AAD Groups (indicating progress in steps of $indicator)" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { $aadGroupIdWithRoleAssignment = $_ @@ -105,13 +105,13 @@ function processAADGroups { $processedAADGroupsCount = ($arrayProgressedAADGroups).Count if ($processedAADGroupsCount) { if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount AAD Groups processed" + Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" } } } -ThrottleLimit ($ThrottleLimit * 2) } else { - Write-Host " processing $($aadGroupsCount) AAD Groups" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" } $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count @@ -119,8 +119,8 @@ function processAADGroups { Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" } - Write-Host " processed $($arrayProgressedAADGroups.Count) AAD Groups" + Write-Host " processed $($arrayProgressedAADGroups.Count) Microsoft Entra groups" $endAADGroupsResolveMembers = Get-Date - Write-Host "Resolving AAD Groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving AAD Groups)" + Write-Host "Resolving Microsoft Entra groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after resolving Microsoft Entra groups)" } \ No newline at end of file diff --git a/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 b/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 index 14caed8b..4d5a5d61 100644 --- a/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 +++ b/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 @@ -2161,7 +2161,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
diff --git a/pwsh/dev/functions/processTenantSummary.ps1 b/pwsh/dev/functions/processTenantSummary.ps1 index 1cc84c62..5dbe069a 100644 --- a/pwsh/dev/functions/processTenantSummary.ps1 +++ b/pwsh/dev/functions/processTenantSummary.ps1 @@ -6233,7 +6233,7 @@ extensions: [{ name: 'sort' }] $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = $null $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = foreach ($highPrivilegedGuestUserRoleAssignment in ($highPrivilegedGuestUserRoleAssignments)) { if ($highPrivilededGuestUserRoleAssignment.AssignmentType -eq 'indirect') { - $assignmentInfo = "indirect / AAD Group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" + $assignmentInfo = "indirect / Microsoft Entra group membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" } else { $assignmentInfo = 'direct' @@ -6910,7 +6910,7 @@ extensions: [{ name: 'sort' }] $tfCount = $summarySubscriptionsCount $htmlTableId = 'TenantSummary_subs' - $abbr = " " + $abbr = " " [void]$htmlTenantSummary.AppendLine(@"
@@ -8880,7 +8880,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
diff --git a/pwsh/dev/functions/runInfo.ps1 b/pwsh/dev/functions/runInfo.ps1 index 49da6ac5..4f0bceb2 100644 --- a/pwsh/dev/functions/runInfo.ps1 +++ b/pwsh/dev/functions/runInfo.ps1 @@ -96,7 +96,7 @@ function runInfo { } if (-not $NoAADGroupsResolveMembers) { - Write-Host " AAD Groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving AAD Group memberships" -ForegroundColor Yellow + Write-Host " Microsoft Entra groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving group memberships" -ForegroundColor Yellow $script:paramsUsed += 'NoAADGroupsResolveMembers: false ' if ($AADGroupMembersLimit -eq 500) { Write-Host " AADGroupMembersLimit = $AADGroupMembersLimit" -ForegroundColor Yellow @@ -108,7 +108,7 @@ function runInfo { } } else { - Write-Host " AAD Groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green + Write-Host " Microsoft Entra groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green $script:paramsUsed += 'NoAADGroupsResolveMembers: true ' } From cc7f7efc7b96a4ad759d121ef2adf5af4d0fe2f3 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 10 Jan 2024 14:45:36 +0000 Subject: [PATCH 11/18] casing --- .azuredevops/pipelines/AzGovViz.pipeline.yml | 2 +- README.md | 2 +- history.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.azuredevops/pipelines/AzGovViz.pipeline.yml b/.azuredevops/pipelines/AzGovViz.pipeline.yml index a54545d8..a4bc6f30 100644 --- a/.azuredevops/pipelines/AzGovViz.pipeline.yml +++ b/.azuredevops/pipelines/AzGovViz.pipeline.yml @@ -20,7 +20,7 @@ schedules: - master #CHECK branch 'master' is applicable? - delete me :) #Running AzOps? Run Azure Governance Visualizer after 'AzOps - Push' .. -#AzOps Accelerator https://github.com/Azure/AzOps-Accelerator +#AzOps accelerator https://github.com/Azure/AzOps-Accelerator #resources: # pipelines: # - pipeline: 'Push' diff --git a/README.md b/README.md index c201e0eb..37a8f55f 100644 --- a/README.md +++ b/README.md @@ -606,7 +606,7 @@ Azure Governance Visualizer polls the following APIs ## Integrate with AzOps -Did you know you can run AzOps from Azure DevOps? Check [AzOps Accelerator](https://github.com/Azure/AzOps-Accelerator). +Did you know you can run AzOps from Azure DevOps? Check [AzOps accelerator](https://github.com/Azure/AzOps-Accelerator). You can integrate Azure Governance Visualizer (same project as AzOps). ```yaml diff --git a/history.md b/history.md index 7e47bd9a..bb34180d 100644 --- a/history.md +++ b/history.md @@ -73,7 +73,7 @@ __Changes__ (2023-Jul-17) * update to Azure DevOps Pipeline v6_major_20230717_1 [AzGovViz.pipeline.yml](./.azuredevops/pipelines/AzGovViz.pipeline.yml) * Output of published WebApp URL -* update README.md and setup.md - add reference to the [Azure Governance Visualizer Accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) +* update README.md and setup.md - add reference to the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) __Changes__ (2023-Jun-23 / 6.2.3 Minor) @@ -729,7 +729,7 @@ __Changes__ (2021-Aug-30 / Major) * Adding feature for RBAC Role assignments: determine 'standing' from PIM (Privileged Identity Mangement) managed Role assignments * New parameter `-NoResources` - this will speed up the processing time but information like Resource diagnostics capability and resource type stats will not be made available (featured for large tenants) -* Integrate AzGovViz with AzOps (after 'AzOps - Push' run AzGovViz) - (line 77 AzGovViz.yml). Checkout [AzOps Accellerator](https://github.com/Azure/AzOps-Accelerator) +* Integrate AzGovViz with AzOps (after 'AzOps - Push' run AzGovViz) - (line 77 AzGovViz.yml). Checkout [AzOps accelerator](https://github.com/Azure/AzOps-Accelerator) * Performance optimization __Changes__ (2021-Aug-25 / Major) From d8aef7b637d090ba3f847bc69272ddb0e8ab0c8a Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 10 Jan 2024 14:50:15 +0000 Subject: [PATCH 12/18] remove extra instruction --- run-from/console.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/run-from/console.md b/run-from/console.md index a0cd9e83..ec7253e1 100644 --- a/run-from/console.md +++ b/run-from/console.md @@ -3,8 +3,6 @@ When trying out Azure Governance Visualizer for the first time or simply as a one-time evaluation of an Azure tenant, the quickest way to get results is to run it directly from the console. These instructions will get you up and running from a terminal. -Some steps have both **portal based** ( :computer_mouse: ) and **PowerShell based** ( :keyboard: ) instructions. Use whichever you feel is appropriate for your situation, they both will produce the same results. - ## Prerequisites The following must be installed on the workstation that will be used to run the scripts: @@ -81,7 +79,7 @@ If that permission is not yet assigned to your user or the service principal, a Follow the instructions at [Assign Azure roles using the Azure portal](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-portal) to grant Azure RBAC '**Reader**' role to the management group. -**:keyboard: Use PowerShell to assign the role:** +**:keyboard: Or use PowerShell to assign the role:** ```powershell $objectId = "" From 983e67738c8f838a9c7024ee5dbd84dcae475d22 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 10 Jan 2024 19:36:03 +0000 Subject: [PATCH 13/18] reverting changes in auto-generated file. --- pwsh/AzGovVizParallel.ps1 | 222 +++++++++++++++++++------------------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 3d4275e1..7cc6a6d7 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -127,7 +127,7 @@ Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size) .PARAMETER AADGroupMembersLimit - Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolveds + Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved .PARAMETER NoResources Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -302,7 +302,7 @@ Note if you use parameter -LargeTenant then parameter -NoScopeInsights will be set to true PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoScopeInsights - Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolved + Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -AADGroupMembersLimit 750 Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -578,14 +578,14 @@ Param [switch] $ShowRunIdentifier, - #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits + #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits [int] $LimitRBACCustomRoleDefinitionsTenant = 5000, [int] $LimitRBACRoleAssignmentsManagementGroup = 500, - #https://learn.microsoft.com/azure/governance/policy/overview#maximum-count-of-azure-policy-objects + #https://docs.microsoft.com/en-us/azure/governance/policy/overview#maximum-count-of-azure-policy-objects [int] $LimitPOLICYPolicyAssignmentsManagementGroup = 200, @@ -613,7 +613,7 @@ Param [int] $LimitPOLICYPolicySetDefinitionsScopedSubscription = 200, - #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits + #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits [int] $LimitResourceGroups = 980, @@ -2592,7 +2592,7 @@ function getConsumption { #$subscriptionIdsOptimizedForBody = '"{0}"' -f ($subsToProcessInCustomDataCollection.subscriptionId -join '","') $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for $($subsToProcessInCustomDataCollectionCount) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://learn.microsoft.com/rest/api/cost-management/query/usage + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' @@ -2743,7 +2743,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" #test Write-Host $currentTask - #https://learn.microsoft.com/rest/api/cost-management/query/usage + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -2804,7 +2804,7 @@ function getConsumption { #region mgScope $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://learn.microsoft.com/rest/api/cost-management/query/usage + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $body = @" @@ -2935,7 +2935,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" #test Write-Host $currentTask - #https://learn.microsoft.com/rest/api/cost-management/query/usage + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -3156,7 +3156,7 @@ function getConsumption { function getDefaultManagementGroup { $currentTask = 'Get Default Management Group' Write-Host $currentTask - #https://learn.microsoft.com/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group + #https://docs.microsoft.com/en-us/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2020-02-01" $method = 'GET' $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask @@ -3465,7 +3465,7 @@ function getMDfCSecureScoreMG { $start = Get-Date $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' Write-Host $currentTask - #ref: https://learn.microsoft.com/azure/governance/management-groups/resource-graph-samples#secure-score-per-management-group + #ref: https://docs.microsoft.com/en-us/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -3607,7 +3607,7 @@ function getOrphanedResources { $subsToProcessInCustomDataCollection = $using:subsToProcessInCustomDataCollection $azAPICallConf = $using:azAPICallConf - #Batching: https://learn.microsoft.com/azure/governance/resource-graph/troubleshoot/general#toomanysubscription + #Batching: https://docs.microsoft.com/en-us/azure/governance/resource-graph/troubleshoot/general#toomanysubscription $counterBatch = [PSCustomObject] @{ Value = 0 } $batchSize = 1000 $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } @@ -3939,7 +3939,7 @@ function getPolicyHash { function getPolicyRemediation { $currentTask = 'Getting NonCompliant (dine/modify)' Write-Host $currentTask - #ref: https://learn.microsoft.com/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources + #ref: https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -4287,13 +4287,13 @@ function prepareData { } function processAADGroups { if ($NoPIMEligibility) { - Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment exists)' + Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment exists)' } else { - Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment or PIM eligibility exists)' + Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment or PIM Eligibility exists)' } - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before resolving Microsoft Entra groups)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving AAD Groups)" $startAADGroupsResolveMembers = Get-Date $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique @@ -4305,7 +4305,7 @@ function processAADGroups { } $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount groups from role assignments" + Write-Host " $aadGroupsCount Groups from RoleAssignments" if (-not $NoPIMEligibility) { $PIMEligibleGroups = $arrayPIMEligible.where({ $_.IdentityType -eq 'Group' }) | Select-Object IdentityObjectId, IdentityDisplayName | Sort-Object -Property IdentityObjectId -Unique @@ -4321,9 +4321,9 @@ function processAADGroups { }) } } - Write-Host " $cntPIMEligibleGroupsTotal groups from PIM eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in role assignments)" + Write-Host " $cntPIMEligibleGroupsTotal Groups from PIM Eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments Groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in RoleAssignments)" $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount Groups from role assignments and PIM eligibility" + Write-Host " $aadGroupsCount Groups from RoleAssignments and PIM Eligibility" } if ($aadGroupsCount -gt 0) { @@ -4339,7 +4339,7 @@ function processAADGroups { { $_ -gt 10000 } { $indicator = 250 } } - Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" + Write-Host " processing $($aadGroupsCount) AAD Groups (indicating progress in steps of $indicator)" $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { $aadGroupIdWithRoleAssignment = $_ @@ -4392,24 +4392,24 @@ function processAADGroups { $processedAADGroupsCount = ($arrayProgressedAADGroups).Count if ($processedAADGroupsCount) { if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" + Write-Host " $processedAADGroupsCount AAD Groups processed" } } } -ThrottleLimit ($ThrottleLimit * 2) } else { - Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" + Write-Host " processing $($aadGroupsCount) AAD Groups" } $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count if ($arrayGroupRequestResourceNotFoundCount -gt 0) { - Write-Host "$arrayGroupRequestResourceNotFoundCount groups could not be checked for memberships" + Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" } - Write-Host " processed $($arrayProgressedAADGroups.Count) Microsoft Entra groups" + Write-Host " processed $($arrayProgressedAADGroups.Count) AAD Groups" $endAADGroupsResolveMembers = Get-Date - Write-Host "Resolving Microsoft Entra groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving Microsoft Entra groups)" + Write-Host "Resolving AAD Groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving AAD Groups)" } function processALZPolicyVersionChecker { $start = Get-Date @@ -8096,7 +8096,7 @@ function processNetwork { $Mask = $AddressPrefix.substring($AddressPrefix.Length - 2, 2) #Amount of available IP Addresses minus the 3 IPs that Azure consumes, minus net and broadcast - #https://learn.microsoft.com/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets + #https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets switch ($Mask) { '30' { $AvailableAddresses = [Math]::Pow(2, 2) - 5 } '29' { $AvailableAddresses = [Math]::Pow(2, 3) - 5 } @@ -8605,7 +8605,7 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc - + @@ -8633,18 +8633,18 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +
"@) if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
+    Using deprecated plan 'Container registries' docs
'@) } if ($defenderPlanSubscriptionDeprecatedKubernetesService) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
+    Using deprecated plan 'Kubernetes' docs
'@) } @@ -8746,7 +8746,7 @@ tf.init();}} if ($subscriptionSkippedMDfC.Count -gt 0) { if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs "@) } else { @@ -8758,7 +8758,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs + No Microsoft Defender for Cloud plans docs '@) } } @@ -8900,7 +8900,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs + No Subscription Diagnostic settings docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9022,7 +9022,7 @@ extensions: [{ name: 'sort' }]
-   Resource naming and tagging decision guide docs
+   Resource naming and tagging decision guide docs
   Download CSV semicolon | comma
Subscription Path: $subPath
State: $subscriptionState
QuotaId: $subscriptionQuotaId
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState
Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity
Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles
@@ -9096,7 +9096,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs + Tag Name Usage ($tagsUsageCount Tags) docs "@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9380,7 +9380,7 @@ extensions: [{ name: 'sort' }]
-   Set up preview features in Azure subscription docs +   Set up preview features in Azure subscription docs
@@ -9446,7 +9446,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs + 0 enabled Subscription Features docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9473,7 +9473,7 @@ extensions: [{ name: 'sort' }]
-   Considerations before applying locks docs +   Considerations before applying locks docs
@@ -9541,7 +9541,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs + 0 Resource Locks docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9559,7 +9559,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@" - +
$(($mgAllChildMgs).count -1) ManagementGroups below this scope
$(($mgAllChildSubscriptions).count) Subscriptions below this scope
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
"@) @@ -9695,7 +9695,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs + No Management Group Diagnostic settings docs '@) } #endregion ScopeInsightsDiagnosticsMg @@ -10070,7 +10070,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@"
-   CAF - Recommended abbreviations for Azure resource types docs
+   CAF - Recommended abbreviations for Azure resource types docs
   Resource details can be found in the CSV output *_ResourcesAll.csv
   Download CSV semicolon | comma @@ -10621,7 +10621,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
@@ -18990,7 +18990,7 @@ extensions: [{ name: 'sort' }] $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = $null $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = foreach ($highPrivilegedGuestUserRoleAssignment in ($highPrivilegedGuestUserRoleAssignments)) { if ($highPrivilededGuestUserRoleAssignment.AssignmentType -eq 'indirect') { - $assignmentInfo = "indirect / Microsoft Entra group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" + $assignmentInfo = "indirect / AAD Group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" } else { $assignmentInfo = 'direct' @@ -19617,14 +19617,14 @@ extensions: [{ name: 'sort' }] #region SUMMARYMGdefault Write-Host ' processing TenantSummary ManagementGroups - default Management Group' [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

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

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

+

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

"@) #endregion SUMMARYMGRequireAuthorizationForGroupCreation @@ -19667,12 +19667,12 @@ extensions: [{ name: 'sort' }] $tfCount = $summarySubscriptionsCount $htmlTableId = 'TenantSummary_subs' - $abbr = " " + $abbr = " " [void]$htmlTenantSummary.AppendLine(@"
- Supported Microsoft Azure offers docs
- Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
+ Supported Microsoft Azure offers docs
+ Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
Download CSV semicolon | comma
@@ -20076,7 +20076,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Resource naming and tagging decision guide docs
+ Resource naming and tagging decision guide docs
Download CSV semicolon | comma
@@ -20151,7 +20151,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

Tag Name Usage ($tagsUsageCount Tags) docs

+

Tag Name Usage ($tagsUsageCount Tags) docs

"@) } #endregion SUMMARYTagNameUsage @@ -20477,7 +20477,7 @@ extensions: [{ name: 'sort' }]
- CAF - Recommended abbreviations for Azure resource types docs
+ CAF - Recommended abbreviations for Azure resource types docs
Resource details can be found in the CSV output *_ResourcesAll.csv
Download CSV semicolon | comma
@@ -21086,7 +21086,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Set up preview features in Azure subscription docs
+ Set up preview features in Azure subscription docs
Download CSV semicolon | comma
@@ -21163,7 +21163,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No enabled Subscriptions Features docs

+

No enabled Subscriptions Features docs

'@) } $endSubFeatures = Get-Date @@ -21190,7 +21190,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Considerations before applying locks docs
+ Considerations before applying locks docs
Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv
@@ -21259,7 +21259,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No Resource Locks at all docs

+

No Resource Locks at all docs

'@) } $endResourceLocks = Get-Date @@ -21279,8 +21279,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Register Resource Provider 'Microsoft.Security' docs
- Microsoft Defender for Cloud's enhanced security features docs
+ Register Resource Provider 'Microsoft.Security' docs
+ Microsoft Defender for Cloud's enhanced security features docs
Download CSV semicolon | comma
@@ -21378,17 +21378,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21460,17 +21460,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21637,7 +21637,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
@@ -23102,7 +23102,7 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { [void]$htmlTenantSummary.AppendLine(@"
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23240,7 +23240,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Management Groups configured for Diagnostic settings docs

+

No Management Groups configured for Diagnostic settings docs

'@) } @@ -23249,9 +23249,9 @@ extensions: [{ name: 'sort' }] $tfCount = $arrayMgsWithoutDiagnosticsCount $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' [void]$htmlTenantSummary.AppendLine(@" - +
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23326,7 +23326,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Management Groups are configured for Diagnostic settings docs

+

All Management Groups are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsManagementGroups @@ -23346,7 +23346,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23480,7 +23480,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Subscriptions configured for Diagnostic settings docs

+

No Subscriptions configured for Diagnostic settings docs

'@) } @@ -23491,7 +23491,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23566,7 +23566,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Subscriptions are configured for Diagnostic settings docs

+

All Subscriptions are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsSubscriptions @@ -23593,7 +23593,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs
+ Supported categories for Azure Resource Logs docs
Download CSV semicolon | comma
@@ -23917,7 +23917,7 @@ extensions: [{ name: 'sort' }] else { $resourceCount = '0' } - $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ Priority = '2-Medium' PolicyId = 'n/a' @@ -23952,7 +23952,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs + Supported categories for Azure Resource Logs docs
@@ -23984,7 +23984,7 @@ extensions: [{ name: 'sort' }] $($diagnosticsFinding.Recommendation)
- $($diagnosticsFinding.ResourceType) + $($diagnosticsFinding.ResourceType) $($diagnosticsFinding.ResourceTypeCount) @@ -24129,24 +24129,24 @@ extensions: [{ name: 'sort' }] #policySets if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { [void]$htmlTenantSummary.AppendLine(@" -

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

"@) } @@ -24166,7 +24166,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma @@ -24239,7 +24239,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyAssignments @@ -24253,7 +24253,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24326,7 +24326,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyScope @@ -24340,7 +24340,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24413,7 +24413,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicySetScope @@ -24428,7 +24428,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -24501,7 +24501,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsRoleAssignment @@ -24522,7 +24522,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Resource Group Limit docs
+ Azure Subscription Resource Group Limit docs
Download CSV semicolon | comma
@@ -24596,7 +24596,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

+ $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

"@) } #endregion SUMMARYSubsapproachingLimitsResourceGroups @@ -24610,7 +24610,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Tag Limit docs
+ Azure Subscription Tag Limit docs
Download CSV semicolon | comma
@@ -24683,7 +24683,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsSubscriptionTags @@ -24697,7 +24697,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24770,7 +24770,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyAssignments @@ -24784,7 +24784,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24857,7 +24857,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyScope @@ -24871,7 +24871,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24944,7 +24944,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicySetScope @@ -24961,7 +24961,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -25034,7 +25034,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

+ $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

"@) } #endregion SUMMARYSubsapproachingLimitsRoleAssignment @@ -25275,7 +25275,7 @@ tf.init();}} } } else { - #https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.regex.escape + #https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.escape $s1 = $altName -replace '.*/providers/' $rm = $s1 -replace '.*/' $resourceType = $s1 -replace "/$([System.Text.RegularExpressions.Regex]::Escape($rm))" @@ -28143,7 +28143,7 @@ function runInfo { } if (-not $NoAADGroupsResolveMembers) { - Write-Host " Microsoft Entra groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving group memberships" -ForegroundColor Yellow + Write-Host " AAD Groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving AAD Group memberships" -ForegroundColor Yellow $script:paramsUsed += 'NoAADGroupsResolveMembers: false ' if ($AADGroupMembersLimit -eq 500) { Write-Host " AADGroupMembersLimit = $AADGroupMembersLimit" -ForegroundColor Yellow @@ -28155,7 +28155,7 @@ function runInfo { } } else { - Write-Host " Microsoft Entra groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green + Write-Host " AAD Groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green $script:paramsUsed += 'NoAADGroupsResolveMembers: true ' } @@ -29265,7 +29265,7 @@ function dataCollectionDefenderPlans { ) $currentTask = "Getting Microsoft Defender for Cloud plans for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://learn.microsoft.com/rest/api/securitycenter/pricings + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/pricings?api-version=2018-06-01" $method = 'GET' $defenderPlansResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29350,7 +29350,7 @@ function dataCollectionDefenderEmailContacts { ) $currentTask = "Getting Microsoft Defender for Cloud Email contacts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://learn.microsoft.com/rest/api/securitycenter/pricings + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview" $method = 'GET' $defenderSecurityContactsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -listenOn 'Content' -currentTask $currentTask -caller 'CustomDataCollection' @@ -29447,7 +29447,7 @@ function dataCollectionVNets { ) $currentTask = "Getting Virtual Networks for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://learn.microsoft.com/rest/api/securitycenter/pricings + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/virtualNetworks?api-version=2022-05-01" $method = 'GET' $networkResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29474,7 +29474,7 @@ function dataCollectionPrivateEndpoints { ) $currentTask = "Getting Private Endpoints for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://learn.microsoft.com/rest/api/securitycenter/pricings + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/privateEndpoints?api-version=2022-05-01" $method = 'GET' $privateEndpointsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -unhandledErrorAction Continue @@ -33674,7 +33674,7 @@ function processScopeInsights($mgChild, $mgChildOf) { "@ if ($mgId -eq $defaultManagementGroupId) { $script:html += @' -
+ '@ } $script:html += @" From 407019dba30c8bb94716417b03e2c88ad5400593 Mon Sep 17 00:00:00 2001 From: Julian Hayward Date: Mon, 5 Feb 2024 12:36:50 +0100 Subject: [PATCH 14/18] 6.3.71 --- README.md | 29 +- contributionGuide.md | 1 + pwsh/AzGovVizParallel.ps1 | 224 +- pwsh/dev/devAzGovVizParallel.ps1 | 33210 +------------------------- setup.md | 13 +- {run-from => setup}/azure-devops.md | 0 setup/azure-web-app.md | 34 + {run-from => setup}/console.md | 0 {run-from => setup}/github.md | 0 version.json | 2 +- 10 files changed, 241 insertions(+), 33272 deletions(-) rename {run-from => setup}/azure-devops.md (100%) create mode 100644 setup/azure-web-app.md rename {run-from => setup}/console.md (100%) rename {run-from => setup}/github.md (100%) diff --git a/README.md b/README.md index 37a8f55f..510bf8c8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,15 @@ _Do you want to get granular insights on your technical Azure Governance implementation and document it in CSV, HTML, Markdown, and JSON?_ -Azure Governance Visualizer is a PowerShell based script that iterates your Azure tenant's management group hierarchy down to the subscription level. It captures most relevant Azure governance capabilities such as Azure Policy, RBAC, 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 Azure 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 through your Azure Tenant's Management Group hierarchy, starting from the root Management Group down to the Subscription, Resource Group and Resource level. It collects data from various Azure APIs including Azure ARM, Microsoft Graph and Storage. + +From the collected data it generates enriched insights for capabilities such as Azure Policy, RBAC, and a lot more. + +Within an HTML output it provides visibility on your __HierarchyMap__, creates a __TenantSummary__, creates __DefinitionInsights__ and builds granular __ScopeInsights__ on Azure Management Groups and Subscriptions. + +Further, CSV exports with enriched information per capability will be generated and detailed JSON files are exported which document your entire Azure tenant setup for Management Groups, Subscriptions, Azure RBAC definitions and assignments, Azure policy definitions and assignments. These exports come in handy for change tracking scenarios as well as redeployment of configuration (e.g. tenant migration scenrio) and can even serve as a backup. + +The technical requirements as well as the required permissions are minimal. You can run the script either for your tenant root management group or any other management group. @@ -24,9 +32,8 @@ Azure Governance Visualizer is intended to help you to get a holistic overview o * [Azure Governance Visualizer aka AzGovViz](#azure-governance-visualizer-aka-azgovviz) * [Mission](#mission) * [Table of contents](#table-of-contents) - * [Azure Governance Visualizer @ Microsoft CAF \& WAF](#azure-governance-visualizer--microsoft-caf--waf) + * [Azure Governance Visualizer @ Microsoft CAF](#azure-governance-visualizer--microsoft-caf) * [Microsoft Cloud Adoption Framework (CAF)](#microsoft-cloud-adoption-framework-caf) - * [Microsoft Well Architected Framework (WAF)](#microsoft-well-architected-framework-waf) * [Azure Governance Visualizer accelerator](#azure-governance-visualizer-accelerator) * [ChatGPT](#chatgpt) * [:rocket: Azure Governance Visualizer deployment guide](#rocket-azure-governance-visualizer-deployment-guide) @@ -57,17 +64,13 @@ Azure Governance Visualizer is intended to help you to get a holistic overview o * [AzADServicePrincipalInsights](#azadserviceprincipalinsights) * [Closing Note](#closing-note) -## Azure Governance Visualizer @ Microsoft CAF & WAF +## Azure Governance Visualizer @ Microsoft CAF ### Microsoft Cloud Adoption Framework (CAF) * Listed as [tool](https://learn.microsoft.com/azure/cloud-adoption-framework/resources/tools-templates#govern) for the Govern discipline in the Microsoft Cloud Adoption Framework. * Included in the Cloud Adoption Framework's [Strategy-Plan-Ready-Governance](https://azuredevopsdemogenerator.azurewebsites.net/?name=strategyplan) Azure DevOps Demo Generator template. -### Microsoft Well Architected Framework (WAF) - -* Listed as [security monitoring tool](https://learn.microsoft.com/azure/architecture/framework/security/monitor-tools) in the Microsoft Well Architected Framework - ### Azure Governance Visualizer accelerator The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) provides an easy and fast deployment process that automates the creation and publishing of AzGovViz to an Azure Web Application and provides automation to configuring the pre-requisites for AzGovViz. @@ -78,9 +81,9 @@ The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Gov ## :rocket: Azure Governance Visualizer deployment guide -The instructions to deploy the Azure Governance Visualizer is found in the __[Azure Governance Visualizer (AzGovViz) deployment guide](setup.md)__. Follow those instructions to run AzGovViz from your terminal, Azure DevOps, or GitHub. +The instructions to deploy the Azure Governance Visualizer is found in the __[Azure Governance Visualizer (AzGovViz) deployment guide](setup.md)__. Follow those instructions to run AzGovViz from your terminal (console), GitHub Codepaces, Azure DevOps, or GitHub. -Additionally, you can use the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) to provide HTML output access through Azure Web Apps. +As an alternative, you can use the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) to deploy the Azure Governance Visualizer per code. ## Release history @@ -365,8 +368,8 @@ VMConnection These permissions are __mandatory__ in each and every scenario! -| Scenario | Permissions | -| :------- | :---------- | +| Scenario | Permissions | +| :------- | :------------------------------------------------- | | ALL | '__Reader__' role assignment on _management group_ | ### Required permissions in Microsoft Entra ID @@ -545,7 +548,7 @@ Azure Governance Visualizer polls the following APIs | Endpoint | API version | API name | | -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -| MS Graph | beta | /groups/`entraGroupId`/transitiveMembers | +| MS Graph | beta | /groups/`entraGroupId`/transitiveMembers | | MS Graph | beta | /privilegedAccess/azureResources/resources | | MS Graph | beta | /privilegedAccess/azureResources/roleAssignments | | MS Graph | v1.0 | /applications | diff --git a/contributionGuide.md b/contributionGuide.md index f14d3a47..4f15edee 100644 --- a/contributionGuide.md +++ b/contributionGuide.md @@ -5,6 +5,7 @@ 1. In the folder `.\pwsh\dev` find the function you intend to work on and apply your changes. 1. Edit the file `.\pwsh\dev\devAzGovVizParallel.ps1`. - In the param block update the parameter variable `$ProductVersion` accordingly. + - Note: Do not change anything else in this file if you did not introduce new functions! 1. Execute `.\pwsh\dev\buildAzGovVizParallel.ps1` - This step will rebuilt the main `.\pwsh\AzGovVizParallel.ps1` file, incorporating all changes you did in the `.\pwsh\dev` directory. 1. Edit the file `.\README.md`. - Update the region `Release history`, replace the changes from the previous release with your changes. diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 7cc6a6d7..9ea96a6c 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -62,7 +62,7 @@ use this parameter if Azure Consumption data should not be exported (CSV) .PARAMETER ThrottleLimit - Leveraging PowerShell Core´s parallel capability you can define the ThrottleLimit (default=5) + Leveraging PowerShell Core's parallel capability you can define the ThrottleLimit (default=5) .PARAMETER DoTranscript Log the console output @@ -127,7 +127,7 @@ Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size) .PARAMETER AADGroupMembersLimit - Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved + Defines the limit (default=500) of Microsoft Entra group members; For Microsoft Entra groups that have more members than the defined limit group members will not be resolved .PARAMETER NoResources Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -236,7 +236,7 @@ Define for which time period (days) Azure Consumption data should be gathered; e.g. 14 days; default is 1 day PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -AzureConsumptionPeriod 14 - Define the number of script blocks running in parallel. Leveraging PowerShell Core´s parallel capability you can define the ThrottleLimit (default=5) + Define the number of script blocks running in parallel. Leveraging PowerShell Core's parallel capability you can define the ThrottleLimit (default=5) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -ThrottleLimit 10 Define if you want to log the console output @@ -279,7 +279,7 @@ If the parameter switch is true then the following parameters will be set: -PolicyAtScopeOnly $true -RBACAtScopeOnly $true - - NoResourceProvidersAtAll $true + -NoResourceProvidersAtAll $true -NoScopeInsights $true PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -LargeTenant @@ -302,7 +302,7 @@ Note if you use parameter -LargeTenant then parameter -NoScopeInsights will be set to true PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoScopeInsights - Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved +Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolved PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -AADGroupMembersLimit 750 Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -365,7 +365,7 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.3.7', + $ProductVersion = '6.3.71', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -578,14 +578,14 @@ Param [switch] $ShowRunIdentifier, - #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits + #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits [int] $LimitRBACCustomRoleDefinitionsTenant = 5000, [int] $LimitRBACRoleAssignmentsManagementGroup = 500, - #https://docs.microsoft.com/en-us/azure/governance/policy/overview#maximum-count-of-azure-policy-objects + #https://learn.microsoft.com/azure/governance/policy/overview#maximum-count-of-azure-policy-objects [int] $LimitPOLICYPolicyAssignmentsManagementGroup = 200, @@ -613,7 +613,7 @@ Param [int] $LimitPOLICYPolicySetDefinitionsScopedSubscription = 200, - #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits + #https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits [int] $LimitResourceGroups = 980, @@ -2592,7 +2592,7 @@ function getConsumption { #$subscriptionIdsOptimizedForBody = '"{0}"' -f ($subsToProcessInCustomDataCollection.subscriptionId -join '","') $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for $($subsToProcessInCustomDataCollectionCount) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' @@ -2743,7 +2743,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -2804,7 +2804,7 @@ function getConsumption { #region mgScope $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" Write-Host "$currentTask" - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $body = @" @@ -2935,7 +2935,7 @@ function getConsumption { $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" #test Write-Host $currentTask - #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + #https://learn.microsoft.com/rest/api/cost-management/query/usage $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2023-03-01&`$top=5000" $method = 'POST' $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' @@ -3156,7 +3156,7 @@ function getConsumption { function getDefaultManagementGroup { $currentTask = 'Get Default Management Group' Write-Host $currentTask - #https://docs.microsoft.com/en-us/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group + #https://learn.microsoft.com/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2020-02-01" $method = 'GET' $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask @@ -3465,7 +3465,7 @@ function getMDfCSecureScoreMG { $start = Get-Date $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' Write-Host $currentTask - #ref: https://docs.microsoft.com/en-us/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group + #ref: https://learn.microsoft.com/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -3607,7 +3607,7 @@ function getOrphanedResources { $subsToProcessInCustomDataCollection = $using:subsToProcessInCustomDataCollection $azAPICallConf = $using:azAPICallConf - #Batching: https://docs.microsoft.com/en-us/azure/governance/resource-graph/troubleshoot/general#toomanysubscription + #Batching: https://learn.microsoft.com/azure/governance/resource-graph/troubleshoot/general#toomanysubscription $counterBatch = [PSCustomObject] @{ Value = 0 } $batchSize = 1000 $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } @@ -3939,7 +3939,7 @@ function getPolicyHash { function getPolicyRemediation { $currentTask = 'Getting NonCompliant (dine/modify)' Write-Host $currentTask - #ref: https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources + #ref: https://learn.microsoft.com/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" $method = 'POST' @@ -4287,13 +4287,13 @@ function prepareData { } function processAADGroups { if ($NoPIMEligibility) { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment exists)' } else { - Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment or PIM Eligibility exists)' + Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment or PIM eligibility exists)' } - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving AAD Groups)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before resolving Microsoft Entra groups)" $startAADGroupsResolveMembers = Get-Date $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique @@ -4321,9 +4321,9 @@ function processAADGroups { }) } } - Write-Host " $cntPIMEligibleGroupsTotal Groups from PIM Eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments Groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in RoleAssignments)" + Write-Host " $cntPIMEligibleGroupsTotal groups from PIM eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in role assignments)" $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount Groups from RoleAssignments and PIM Eligibility" + Write-Host " $aadGroupsCount groups from role assignments and PIM eligibility" } if ($aadGroupsCount -gt 0) { @@ -4339,7 +4339,7 @@ function processAADGroups { { $_ -gt 10000 } { $indicator = 250 } } - Write-Host " processing $($aadGroupsCount) AAD Groups (indicating progress in steps of $indicator)" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { $aadGroupIdWithRoleAssignment = $_ @@ -4392,13 +4392,13 @@ function processAADGroups { $processedAADGroupsCount = ($arrayProgressedAADGroups).Count if ($processedAADGroupsCount) { if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount AAD Groups processed" + Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" } } } -ThrottleLimit ($ThrottleLimit * 2) } else { - Write-Host " processing $($aadGroupsCount) AAD Groups" + Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" } $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count @@ -4406,10 +4406,10 @@ function processAADGroups { Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" } - Write-Host " processed $($arrayProgressedAADGroups.Count) AAD Groups" + Write-Host " processed $($arrayProgressedAADGroups.Count) Microsoft Entra groups" $endAADGroupsResolveMembers = Get-Date - Write-Host "Resolving AAD Groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving AAD Groups)" + Write-Host "Resolving Microsoft Entra groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after resolving Microsoft Entra groups)" } function processALZPolicyVersionChecker { $start = Get-Date @@ -8096,7 +8096,7 @@ function processNetwork { $Mask = $AddressPrefix.substring($AddressPrefix.Length - 2, 2) #Amount of available IP Addresses minus the 3 IPs that Azure consumes, minus net and broadcast - #https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets + #https://learn.microsoft.com/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets switch ($Mask) { '30' { $AvailableAddresses = [Math]::Pow(2, 2) - 5 } '29' { $AvailableAddresses = [Math]::Pow(2, 3) - 5 } @@ -8605,7 +8605,7 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc - + @@ -8633,18 +8633,18 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +
"@) if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
+    Using deprecated plan 'Container registries' docs
'@) } if ($defenderPlanSubscriptionDeprecatedKubernetesService) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
+    Using deprecated plan 'Kubernetes' docs
'@) } @@ -8746,7 +8746,7 @@ tf.init();}} if ($subscriptionSkippedMDfC.Count -gt 0) { if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs "@) } else { @@ -8758,7 +8758,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs + No Microsoft Defender for Cloud plans docs '@) } } @@ -8900,7 +8900,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs + No Subscription Diagnostic settings docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9022,7 +9022,7 @@ extensions: [{ name: 'sort' }]
-   Resource naming and tagging decision guide docs
+   Resource naming and tagging decision guide docs
   Download CSV semicolon | comma

Default Management Group docs

Default Management Group docs

Subscription Path: $subPath
State: $subscriptionState
QuotaId: $subscriptionQuotaId
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState
Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity
Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles
@@ -9096,7 +9096,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs + Tag Name Usage ($tagsUsageCount Tags) docs "@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9380,7 +9380,7 @@ extensions: [{ name: 'sort' }]
-   Set up preview features in Azure subscription docs +   Set up preview features in Azure subscription docs
@@ -9446,7 +9446,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs + 0 enabled Subscription Features docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9473,7 +9473,7 @@ extensions: [{ name: 'sort' }]
-   Considerations before applying locks docs +   Considerations before applying locks docs
@@ -9541,7 +9541,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs + 0 Resource Locks docs '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9559,7 +9559,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@" - + - + @@ -8633,18 +8751,18 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +
"@) if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
+    Using deprecated plan 'Container registries' learn
'@) } if ($defenderPlanSubscriptionDeprecatedKubernetesService) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
+    Using deprecated plan 'Kubernetes' learn
'@) } @@ -8746,7 +8864,7 @@ tf.init();}} if ($subscriptionSkippedMDfC.Count -gt 0) { if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) learn "@) } else { @@ -8758,7 +8876,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs + No Microsoft Defender for Cloud plans learn '@) } } @@ -8900,7 +9018,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs + No Subscription Diagnostic settings learn '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9022,7 +9140,7 @@ extensions: [{ name: 'sort' }]
-   Resource naming and tagging decision guide docs
+   Resource naming and tagging decision guide learn
   Download CSV semicolon | comma
$(($mgAllChildMgs).count -1) ManagementGroups below this scope
$(($mgAllChildSubscriptions).count) Subscriptions below this scope
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
"@) @@ -9695,7 +9695,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs + No Management Group Diagnostic settings docs '@) } #endregion ScopeInsightsDiagnosticsMg @@ -10070,7 +10070,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@"
-   CAF - Recommended abbreviations for Azure resource types docs
+   CAF - Recommended abbreviations for Azure resource types docs
   Resource details can be found in the CSV output *_ResourcesAll.csv
   Download CSV semicolon | comma @@ -10621,7 +10621,7 @@ extensions: [{ name: 'sort' }]
-   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Managed identity 'user-assigned' vs 'system-assigned' docs
   Download CSV semicolon | comma
@@ -18990,7 +18990,7 @@ extensions: [{ name: 'sort' }] $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = $null $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = foreach ($highPrivilegedGuestUserRoleAssignment in ($highPrivilegedGuestUserRoleAssignments)) { if ($highPrivilededGuestUserRoleAssignment.AssignmentType -eq 'indirect') { - $assignmentInfo = "indirect / AAD Group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" + $assignmentInfo = "indirect / Microsoft Entra group membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" } else { $assignmentInfo = 'direct' @@ -19617,14 +19617,14 @@ extensions: [{ name: 'sort' }] #region SUMMARYMGdefault Write-Host ' processing TenantSummary ManagementGroups - default Management Group' [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

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

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

+

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

"@) #endregion SUMMARYMGRequireAuthorizationForGroupCreation @@ -19667,12 +19667,12 @@ extensions: [{ name: 'sort' }] $tfCount = $summarySubscriptionsCount $htmlTableId = 'TenantSummary_subs' - $abbr = " " + $abbr = " " [void]$htmlTenantSummary.AppendLine(@"
- Supported Microsoft Azure offers docs
- Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
+ Supported Microsoft Azure offers docs
+ Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
Download CSV semicolon | comma
@@ -20076,7 +20076,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Resource naming and tagging decision guide docs
+ Resource naming and tagging decision guide docs
Download CSV semicolon | comma
@@ -20151,7 +20151,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

Tag Name Usage ($tagsUsageCount Tags) docs

+

Tag Name Usage ($tagsUsageCount Tags) docs

"@) } #endregion SUMMARYTagNameUsage @@ -20477,7 +20477,7 @@ extensions: [{ name: 'sort' }]
- CAF - Recommended abbreviations for Azure resource types docs
+ CAF - Recommended abbreviations for Azure resource types docs
Resource details can be found in the CSV output *_ResourcesAll.csv
Download CSV semicolon | comma
@@ -21086,7 +21086,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Set up preview features in Azure subscription docs
+ Set up preview features in Azure subscription docs
Download CSV semicolon | comma
@@ -21163,7 +21163,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No enabled Subscriptions Features docs

+

No enabled Subscriptions Features docs

'@) } $endSubFeatures = Get-Date @@ -21190,7 +21190,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Considerations before applying locks docs
+ Considerations before applying locks docs
Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv
@@ -21259,7 +21259,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No Resource Locks at all docs

+

No Resource Locks at all docs

'@) } $endResourceLocks = Get-Date @@ -21279,8 +21279,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Register Resource Provider 'Microsoft.Security' docs
- Microsoft Defender for Cloud's enhanced security features docs
+ Register Resource Provider 'Microsoft.Security' docs
+ Microsoft Defender for Cloud's enhanced security features docs
Download CSV semicolon | comma
@@ -21378,17 +21378,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21460,17 +21460,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
+ Using deprecated plan 'Container registries'docs
'@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
+ Using deprecated plan 'Kubernetes'docs
'@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
+ Microsoft Defender for Cloud's enhanced security featuresdocs
Download CSV semicolon | comma
@@ -21637,7 +21637,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Managed identity 'user-assigned' vs 'system-assigned' docs
+ Managed identity 'user-assigned' vs 'system-assigned' docs
Download CSV semicolon | comma
@@ -23102,7 +23102,7 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { [void]$htmlTenantSummary.AppendLine(@"
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23240,7 +23240,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Management Groups configured for Diagnostic settings docs

+

No Management Groups configured for Diagnostic settings docs

'@) } @@ -23249,9 +23249,9 @@ extensions: [{ name: 'sort' }] $tfCount = $arrayMgsWithoutDiagnosticsCount $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' [void]$htmlTenantSummary.AppendLine(@" - +
- Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
Download CSV semicolon | comma
@@ -23326,7 +23326,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Management Groups are configured for Diagnostic settings docs

+

All Management Groups are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsManagementGroups @@ -23346,7 +23346,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23480,7 +23480,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

No Subscriptions configured for Diagnostic settings docs

+

No Subscriptions configured for Diagnostic settings docs

'@) } @@ -23491,7 +23491,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Create diagnostic setting docs
+ Create diagnostic setting docs
Download CSV semicolon | comma
@@ -23566,7 +23566,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

All Subscriptions are configured for Diagnostic settings docs

+

All Subscriptions are configured for Diagnostic settings docs

'@) } #endregion SUMMARYDiagnosticsSubscriptions @@ -23593,7 +23593,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs
+ Supported categories for Azure Resource Logs docs
Download CSV semicolon | comma
@@ -23917,7 +23917,7 @@ extensions: [{ name: 'sort' }] else { $resourceCount = '0' } - $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ Priority = '2-Medium' PolicyId = 'n/a' @@ -23952,7 +23952,7 @@ extensions: [{ name: 'sort' }]
Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
- Supported categories for Azure Resource Logs docs + Supported categories for Azure Resource Logs docs
@@ -23984,7 +23984,7 @@ extensions: [{ name: 'sort' }] $($diagnosticsFinding.Recommendation) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -'@) - #endregion ScopeInsightsRoleAssignments - - - if (-not $NoScopeInsights) { - $script:html += $htmlScopeInsights - } - - if (-not $NoSingleSubscriptionOutput) { - if ($mgOrSub -eq 'sub') { - $htmlThisSubSingleOutput = $htmlSubscriptionOnlyStart - $htmlThisSubSingleOutput += $htmlScopeInsights - $htmlThisSubSingleOutput += $htmlSubscriptionOnlyEnd - $htmlThisSubSingleOutput | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($HTMLPath)$($DirectorySeparatorChar)$($fileName)_$($subscriptionId).html" -Encoding utf8 -Force - $htmlThisSubSingleOutput = $null - } - } - - if (-not $NoScopeInsights) { - if ($scopescnter % 50 -eq 0) { - $script:scopescnter = 0 - Write-Host ' append file duration: '(Measure-Command { $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force }).TotalSeconds 'seconds' - $script:html = $null - } - } - - if ($scopescnter % 50 -eq 0) { - showMemoryUsage - } - -} -function processStorageAccountAnalysis { - $start = Get-Date - Write-Host 'Processing Storage Account Analysis' - $storageAccountsCount = $storageAccounts.count - if ($storageAccountsCount -gt 0) { - Write-Host " Executing Storage Account Analysis for $storageAccountsCount Storage Accounts" - createBearerToken -AzAPICallConfiguration $azapicallconf -targetEndPoint 'Storage' - - $htSACost = @{} - if ($DoAzureConsumption -eq $true) { - $saConsumptionByResourceId = $allConsumptionData.where({ $_.resourceType -eq 'microsoft.storage/storageaccounts' }) | Group-Object -Property resourceid - - foreach ($sa in $saConsumptionByResourceId) { - $htSACost.($sa.Name) = @{} - $htSACost.($sa.Name).meterCategoryAll = ($sa.Group.MeterCategory | Sort-Object) -join ', ' - $htSACost.($sa.Name).costAll = ($sa.Group.PreTaxCost | Measure-Object -Sum).Sum #[decimal]($sa.Group.PreTaxCost | Measure-Object -Sum).Sum - $htSACost.($sa.Name).currencyAll = ($sa.Group.Currency | Sort-Object -Unique) -join ', ' - foreach ($costentry in $sa.Group) { - $htSACost.($sa.Name)."cost_$($costentry.MeterCategory)" = $costentry.PreTaxCost - $htSACost.($sa.Name)."currency_$($costentry.MeterCategory)" = $costentry.Currency - } - } - } - - $storageAccounts | ForEach-Object -Parallel { - $storageAccount = $_ - $azAPICallConf = $using:azAPICallConf - $arrayStorageAccountAnalysisResults = $using:arrayStorageAccountAnalysisResults - $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI - $htSubscriptionsMgPath = $using:htSubscriptionsMgPath - $htSubscriptionTags = $using:htSubscriptionTags - $CSVDelimiterOpposite = $using:CSVDelimiterOpposite - $htSACost = $using:htSACost - $StorageAccountAccessAnalysisSubscriptionTags = $using:StorageAccountAccessAnalysisSubscriptionTags - $StorageAccountAccessAnalysisStorageAccountTags = $using:StorageAccountAccessAnalysisStorageAccountTags - $listContainersSuccess = 'n/a' - $containersCount = 'n/a' - $arrayContainers = @() - $arrayContainersAnonymousContainer = @() - $arrayContainersAnonymousBlob = @() - $staticWebsitesState = 'n/a' - $webSiteResponds = 'n/a' - - $subscriptionId = ($storageAccount.SA.id -split '/')[2] - $resourceGroupName = ($storageAccount.SA.id -split '/')[4] - $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails - - Write-Host "Processing Storage Account '$($storageAccount.SA.name)' - Subscription: '$($subDetails.displayName)' ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)]" - - if ($storageAccount.SA.Properties.primaryEndpoints.blob) { - - $urlServiceProps = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?restype=service&comp=properties" - $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get restype=service&comp=properties" -saResourceGroupName $resourceGroupName -unhandledErrorAction Continue - if ($saProperties) { - if ($saProperties -eq 'AuthorizationFailure' -or $saProperties -eq 'AuthorizationPermissionDenied' -or $saProperties -eq 'ResourceUnavailable' -or $saProperties -eq 'AuthorizationPermissionMismatch' ) { - if ($saProperties -eq 'ResourceUnavailable') { - $staticWebsitesState = $saProperties - } - } - else { - try { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if ($saProperties.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$saProperties - $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) - } - - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions - - if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { - if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { - $staticWebsitesState = $true - } - else { - $staticWebsitesState = $false - } - } - } - catch { - Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.SA.name)" - Write-Host $($saProperties.ForEach({ [char]$_ }) -join '') -ForegroundColor Cyan - } - } - } - - $urlCompList = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?comp=list" - $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get comp=list" -unhandledErrorAction Continue - if ($listContainers) { - if ($listContainers -eq 'AuthorizationFailure' -or $listContainers -eq 'AuthorizationPermissionDenied' -or $listContainers -eq 'ResourceUnavailable' -or $listContainers -eq 'AuthorizationPermissionMismatch') { - if ($listContainers -eq 'ResourceUnavailable') { - $listContainersSuccess = $listContainers - } - else { - $listContainersSuccess = $false - } - } - else { - $listContainersSuccess = $true - } - - if ($listContainersSuccess -eq $true) { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if ($listContainers.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$listContainers - $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) - } - - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions - - $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count - - foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { - $arrayContainers += $container.Name - if ($container.Name -eq '$web' -and $staticWebsitesState) { - if ($storageAccount.SA.properties.primaryEndpoints.web) { - try { - $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.SA.properties.primaryEndpoints.web -Method 'HEAD' - $webSiteResponds = $true - } - catch { - $webSiteResponds = $false - } - } - } - - if ($container.Properties.PublicAccess) { - if ($container.Properties.PublicAccess -eq 'blob') { - $arrayContainersAnonymousBlob += $container.Name - } - if ($container.Properties.PublicAccess -eq 'container') { - $arrayContainersAnonymousContainer += $container.Name - } - } - } - } - } - } - - $allowSharedKeyAccess = $storageAccount.SA.properties.allowSharedKeyAccess - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowSharedKeyAccess)) { - $allowSharedKeyAccess = 'likely True' - } - $requireInfrastructureEncryption = $storageAccount.SA.properties.encryption.requireInfrastructureEncryption - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.encryption.requireInfrastructureEncryption)) { - $requireInfrastructureEncryption = 'likely False' - } - - $arrayResourceAccessRules = [System.Collections.ArrayList]@() - if ($storageAccount.SA.properties.networkAcls.resourceAccessRules) { - if ($storageAccount.SA.properties.networkAcls.resourceAccessRules.count -gt 0) { - foreach ($resourceAccessRule in $storageAccount.SA.properties.networkAcls.resourceAccessRules) { - - $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' - $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" - - [regex]$regex = '\*+' - #$resourceAccessRule.resourceId - switch ($regex.matches($resourceAccessRule.resourceId).count) { - { $_ -eq 1 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'resourceGroup' - sort = 3 - }) - } - { $_ -eq 2 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'subscription' - sort = 2 - }) - } - { $_ -eq 3 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'tenant' - sort = 1 - }) - } - default { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'resource' - resource = $resourceAccessRule.resourceId - sort = 0 - }) - } - } - } - } - } - $resourceAccessRulesCount = $arrayResourceAccessRules.count - if ($resourceAccessRulesCount -eq 0) { - $resourceAccessRules = '' - } - else { - $ht = @{} - foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { - - if ($accessRulePerRange.Name -eq 'resource') { - $arrayResources = @() - foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { - $arrayResources += $resource - } - $ht.($accessRulePerRange.Name) = [array]($arrayResources) - } - else { - $arrayResourceTypes = @() - foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { - $arrayResourceTypes += $resourceType - } - $ht.($accessRulePerRange.Name) = [array]($arrayResourceTypes) - } - } - $resourceAccessRules = $ht | ConvertTo-Json - } - - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.publicNetworkAccess)) { - $publicNetworkAccess = 'likely Enabled' - } - else { - $publicNetworkAccess = $storageAccount.SA.properties.publicNetworkAccess - } - - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowedCopyScope)) { - $allowedCopyScope = 'From any Storage Account' - } - else { - $allowedCopyScope = $storageAccount.SA.properties.allowedCopyScope - } - - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowCrossTenantReplication)) { - if ($allowedCopyScope -ne 'From any Storage Account') { - $allowCrossTenantReplication = "likely False (allowedCopyScope=$allowedCopyScope)" - } - else { - $allowCrossTenantReplication = 'likely True' - } - } - else { - $allowCrossTenantReplication = $storageAccount.SA.properties.allowCrossTenantReplication - } - - if ($storageAccount.SA.properties.dnsEndpointType) { - $dnsEndpointType = $storageAccount.SA.properties.dnsEndpointType - } - else { - $dnsEndpointType = 'standard' - } - - if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { - if ($htSACost.($storageAccount.SA.id)) { - $hlpCost = $htSACost.($storageAccount.SA.id) - $saCost = $hlpCost.costAll - $saCostCurrency = $hlpCost.currencyAll - $saCostMeterCategories = $hlpCost.meterCategoryAll - } - else { - $saCost = 'n/a' - $saCostCurrency = 'n/a' - $saCostMeterCategories = 'n/a' - } - } - else { - $saCost = '' - $saCostCurrency = '' - $saCostMeterCategories = '' - } - - $temp = [System.Collections.ArrayList]@() - $null = $temp.Add([PSCustomObject]@{ - storageAccount = $storageAccount.SA.name - kind = $storageAccount.SA.kind - skuName = $storageAccount.SA.sku.name - skuTier = $storageAccount.SA.sku.tier - location = $storageAccount.SA.location - creationTime = $storageAccount.SA.properties.creationTime - allowBlobPublicAccess = $storageAccount.SA.properties.allowBlobPublicAccess - publicNetworkAccess = $publicNetworkAccess - SubscriptionId = $subscriptionId - SubscriptionName = $subDetails.displayName - subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId - subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' - resourceGroup = $resourceGroupName - networkAclsdefaultAction = $storageAccount.SA.properties.networkAcls.defaultAction - staticWebsitesState = $staticWebsitesState - staticWebsitesResponse = $webSiteResponds - containersCanBeListed = $listContainersSuccess - containersCount = $containersCount - containers = $arrayContainers -join "$CSVDelimiterOpposite " - containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count - containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " - containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count - containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " - ipRulesCount = $storageAccount.SA.properties.networkAcls.ipRules.Count - ipRulesIPAddressList = ($storageAccount.SA.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " - virtualNetworkRulesCount = $storageAccount.SA.properties.networkAcls.virtualNetworkRules.Count - virtualNetworkRulesList = ($storageAccount.SA.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " - resourceAccessRulesCount = $resourceAccessRulesCount - resourceAccessRules = $resourceAccessRules - bypass = ($storageAccount.SA.properties.networkAcls.bypass | Sort-Object) -join "$CSVDelimiterOpposite " - supportsHttpsTrafficOnly = $storageAccount.SA.properties.supportsHttpsTrafficOnly - minimumTlsVersion = $storageAccount.SA.properties.minimumTlsVersion - allowSharedKeyAccess = $allowSharedKeyAccess - requireInfrastructureEncryption = $requireInfrastructureEncryption - allowedCopyScope = $allowedCopyScope - allowCrossTenantReplication = $allowCrossTenantReplication - dnsEndpointType = $dnsEndpointType - usedCapacity = $storageAccount.SAUsedCapacity - cost = $saCost - metercategory = $saCostMeterCategories - curreny = $saCostCurrency - }) - - if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { - foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { - if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { - $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) - } - else { - $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' - } - } - } - - if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { - if ($storageAccount.SA.tags) { - $htAllSATags = @{} - foreach ($saTagName in ($storageAccount.SA.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { - $htAllSATags.$saTagName = $storageAccount.SA.tags.$saTagName - } - } - foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { - if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { - $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) - } - else { - $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' - } - } - } - - $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) - - } -ThrottleLimit $ThrottleLimit - } - else { - Write-Host ' No Storage Accounts present' - } - - $end = Get-Date - Write-Host " Processing Storage Account Analysis duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" -} -function processTenantSummary() { - Write-Host ' Building TenantSummary' - showMemoryUsage - if ($getMgParentName -eq 'Tenant Root') { - $scopeNamingSummary = 'Tenant wide' - } - else { - $scopeNamingSummary = "ManagementGroup '$ManagementGroupId' and descendants wide" - } - - #region tenantSummaryPre - $startRoleAssignmentsAllPre = Get-Date - $roleAssignmentsallCount = ($rbacBaseQuery).count - Write-Host " processing (pre) TenantSummary RoleAssignments (all $roleAssignmentsallCount)" - - #region RelatedPolicyAssignments - $startRelatedPolicyAssignmentsAll = Get-Date - $htRoleAssignmentRelatedPolicyAssignments = @{} - $htOrphanedSPMI = @{} - foreach ($roleAssignmentIdUnique in $roleAssignmentsUniqueById) { - - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId) = @{} - - if ($htManagedIdentityForPolicyAssignment.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId)) { - $hlpPolicyAssignmentId = ($htManagedIdentityForPolicyAssignment.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId).policyAssignmentId).ToLower() - if (-not $htCacheAssignmentsPolicy.($hlpPolicyAssignmentId)) { - if ($ManagementGroupId -eq $azAPICallConf['checkContext'].Tenant.Id) { - if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { - if (-not ($htCacheAssignmentsPolicyOnResourceGroupsAndResources).($hlpPolicyAssignmentId)) { - Write-Host " !Relict detected: SP MI: $($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) - PolicyAssignmentId: $hlpPolicyAssignmentId" - if (-not $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId)) { - $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) = @{} - } - } - } - else { - Write-Host " !Relict detected: SP MI: $($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) - PolicyAssignmentId: $hlpPolicyAssignmentId" - if (-not $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId)) { - $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) = @{} - } - } - } - } - else { - $temp0000000000 = $htCacheAssignmentsPolicy.($hlpPolicyAssignmentId) - $policyAssignmentId = ($temp0000000000.Assignment.id).Tolower() - $policyDefinitionId = ($temp0000000000.Assignment.properties.policyDefinitionId).Tolower() - - - #builtin - if ($policyDefinitionId -like '/providers/Microsoft.Authorization/policy*') { - #policy - if ($policyDefinitionId -like '/providers/Microsoft.Authorization/policyDefinitions/*') { - $LinkOrNotLinkToAzAdvertizer = ($htCacheDefinitionsPolicy).($policyDefinitionId).LinkToAzAdvertizer - } - #policySet - if ($policyDefinitionId -like '/providers/Microsoft.Authorization/policySetDefinitions/*') { - $LinkOrNotLinkToAzAdvertizer = ($htCacheDefinitionsPolicySet).($policyDefinitionId).LinkToAzAdvertizer - } - } - else { - #policy - if ($policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { - $policyDisplayName = ($htCacheDefinitionsPolicy).($policyDefinitionId).DisplayName - - } - #policySet - if ($policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { - $policyDisplayName = ($htCacheDefinitionsPolicySet).($policyDefinitionId).DisplayName - - } - - $LinkOrNotLinkToAzAdvertizer = "$($policyDisplayName -replace '<', '<' -replace '>', '>')" - } - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignment = "$($policyAssignmentId) ($LinkOrNotLinkToAzAdvertizer)" - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignmentClear = "$($policyAssignmentId) ($policyDisplayName)" - } - } - else { - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignment = 'none' - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignmentClear = 'none' - } - - if ($roleAssignmentIdUnique.RoleIsCustom -eq 'FALSE') { - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleType = 'Builtin' - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleWithWithoutLinkToAzAdvertizer = ($htCacheDefinitionsRole).($roleAssignmentIdUnique.RoleDefinitionId).LinkToAzAdvertizer - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleClear = $roleAssignmentIdUnique.RoleDefinitionName - } - else { - - if ($roleAssigned.RoleSecurityCustomRoleOwner -eq 1) { - $roletype = "Custom" - } - else { - $roleType = 'Custom' - } - - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleType = $roleType - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleWithWithoutLinkToAzAdvertizer = $roleAssignmentIdUnique.RoleDefinitionName - $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleClear = $roleAssignmentIdUnique.RoleDefinitionName - } - } - $endRelatedPolicyAssignmentsAll = Get-Date - Write-Host " RelatedPolicyAssignmentsAll duration: $((New-TimeSpan -Start $startRelatedPolicyAssignmentsAll -End $endRelatedPolicyAssignmentsAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startRelatedPolicyAssignmentsAll -End $endRelatedPolicyAssignmentsAll).TotalSeconds) seconds)" - #endregion RelatedPolicyAssignments - - #region createRBACAll - $cnter = 0 - $script:rbacAll = [System.Collections.ArrayList]@() - $startCreateRBACAll = Get-Date - foreach ($rbac in $rbacBaseQuery) { - $cnter++ - if ($cnter % 1000 -eq 0) { - $etappeRoleAssignmentsAll = Get-Date - Write-Host " $cnter of $roleAssignmentsallCount RoleAssignments processed; $((New-TimeSpan -Start $startRoleAssignmentsAllPre -End $etappeRoleAssignmentsAll).TotalSeconds) seconds" - } - $scope = $null - - if ($rbac.RoleAssignmentPIM -eq 'true') { - $pim = $true - $pimAssignmentType = $rbac.RoleAssignmentPIMAssignmentType - $pimSlotStart = [string]$($rbac.RoleAssignmentPIMSlotStart) - $pimSlotEnd = [string]$($rbac.RoleAssignmentPIMSlotEnd) - } - else { - $pim = $false - $pimAssignmentType = '' - $pimSlotStart = '' - $pimSlotEnd = '' - } - - if ($rbac.RoleAssignmentId -like '/providers/Microsoft.Management/managementGroups/*') { - $scopeTenOrMgOrSubOrRGOrRes = 'Mg' - if (-not [String]::IsNullOrEmpty($rbac.SubscriptionId)) { - $scope = "inherited $($rbac.RoleAssignmentScopeName)" - } - else { - if (($rbac.RoleAssignmentScopeName) -eq $rbac.MgId) { - $scope = 'thisScope MG' - } - else { - $scope = "inherited $($rbac.RoleAssignmentScopeName)" - } - } - } - - if ($rbac.RoleAssignmentId -like '/subscriptions/*') { - $scope = 'thisScope Sub' - $scopeTenOrMgOrSubOrRGOrRes = 'Sub' - } - - if ($rbac.RoleAssignmentId -like '/subscriptions/*/resourcegroups/*') { - $scope = 'thisScope Sub RG' - $scopeTenOrMgOrSubOrRGOrRes = 'RG' - } - - if ($rbac.RoleAssignmentId -like '/subscriptions/*/resourcegroups/*/providers/*/providers/*') { - $scope = 'thisScope Sub RG Res' - $scopeTenOrMgOrSubOrRGOrRes = 'Res' - } - - if ($rbac.RoleAssignmentId -like '/providers/Microsoft.Authorization/roleAssignments/*') { - $scope = 'inherited Tenant' - $scopeTenOrMgOrSubOrRGOrRes = 'Ten' - } - - $objectTypeUserType = '' - if ($rbac.RoleAssignmentIdentityObjectType -eq 'User') { - if ($htUserTypesGuest.($rbac.RoleAssignmentIdentityObjectId)) { - $objectTypeUserType = 'Guest' - } - else { - $objectTypeUserType = 'Member' - } - } - - if (-not [string]::IsNullOrEmpty($rbac.RoleDataActions) -or -not [string]::IsNullOrEmpty($rbac.RoleNotDataActions)) { - $roleManageData = 'true' - } - else { - $roleManageData = 'false' - } - - $hlpRoleAssignmentRelatedPolicyAssignments = $htRoleAssignmentRelatedPolicyAssignments.($rbac.RoleAssignmentId) - - if (-not $NoAADGroupsResolveMembers) { - if ($rbac.RoleAssignmentIdentityObjectType -eq 'Group') { - - $grpHlpr = $htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId) - $null = $script:rbacAll.Add([PSCustomObject]@{ - Level = $rbac.Level - RoleAssignmentId = $rbac.RoleAssignmentId - RoleAssignmentPIMRelated = $pim - RoleAssignmentPIMAssignmentType = $pimAssignmentType - RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart - RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd - CreatedBy = $rbac.RoleAssignmentCreatedBy - CreatedOn = $rbac.RoleAssignmentCreatedOn - UpdatedBy = $rbac.RoleAssignmentUpdatedBy - UpdatedOn = $rbac.RoleAssignmentUpdatedOn - MgId = $rbac.MgId - MgName = $rbac.MgName - MgParentId = $rbac.MgParentId - MgParentName = $rbac.MgParentName - SubscriptionId = $rbac.SubscriptionId - SubscriptionName = $rbac.Subscription - Scope = $scope - ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes - RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName - RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG - RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes - Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer - RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear - RoleId = $rbac.RoleDefinitionId - RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType - RoleDataRelated = $roleManageData - AssignmentType = 'direct' - AssignmentInheritFrom = '' - GroupMembersCount = "$($grpHlpr.MembersAllCount) (Usr: $($grpHlpr.MembersUsersCount)$($CsvDelimiterOpposite) Grp: $($grpHlpr.MembersGroupsCount)$($CsvDelimiterOpposite) SP: $($grpHlpr.MembersServicePrincipalsCount))" - ObjectDisplayName = $rbac.RoleAssignmentIdentityDisplayname - ObjectSignInName = $rbac.RoleAssignmentIdentitySignInName - ObjectId = $rbac.RoleAssignmentIdentityObjectId - ObjectType = $rbac.RoleAssignmentIdentityObjectType - RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment - RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear - RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner - RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP - RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments - }) - - - if ($grpHlpr.MembersAllCount -gt 0) { - - if ($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount -le $AADGroupMembersLimit) { - - foreach ($groupmember in $htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAll) { - if ($groupmember.'@odata.type' -eq '#microsoft.graph.user') { - if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $true) { - $grpMemberDisplayName = 'scrubbed' - $grpMemberSignInName = 'scrubbed' - } - else { - $grpMemberDisplayName = $groupmember.displayName - $grpMemberSignInName = $groupmember.userPrincipalName - } - $grpMemberId = $groupmember.Id - $grpMemberType = 'User' - $grpMemberUserType = '' - - if ($htUserTypesGuest.($grpMemberId)) { - $grpMemberUserType = 'Guest' - } - else { - $grpMemberUserType = 'Member' - } - - $identityTypeFull = "$grpMemberType $grpMemberUserType" - } - if ($groupmember.'@odata.type' -eq '#microsoft.graph.group') { - $grpMemberDisplayName = $groupmember.displayName - $grpMemberSignInName = 'n/a' - $grpMemberId = $groupmember.Id - $grpMemberType = 'Group' - $grpMemberUserType = '' - $identityTypeFull = "$grpMemberType" - } - if ($groupmember.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { - $grpMemberDisplayName = $groupmember.appDisplayName - $grpMemberSignInName = 'n/a' - $grpMemberId = $groupmember.Id - $grpMemberType = 'ServicePrincipal' - $grpMemberUserType = '' - $identityType = $htServicePrincipals.($grpMemberId).spTypeConcatinated - $identityTypeFull = "$identityType" - } - - $null = $script:rbacAll.Add([PSCustomObject]@{ - Level = $rbac.Level - RoleAssignmentId = $rbac.RoleAssignmentId - RoleAssignmentPIMRelated = $pim - RoleAssignmentPIMAssignmentType = $pimAssignmentType - RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart - RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd - CreatedBy = $rbac.RoleAssignmentCreatedBy - CreatedOn = $rbac.RoleAssignmentCreatedOn - UpdatedBy = $rbac.RoleAssignmentUpdatedBy - UpdatedOn = $rbac.RoleAssignmentUpdatedOn - MgId = $rbac.MgId - MgName = $rbac.MgName - MgParentId = $rbac.MgParentId - MgParentName = $rbac.MgParentName - SubscriptionId = $rbac.SubscriptionId - SubscriptionName = $rbac.Subscription - Scope = $scope - ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes - RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName - RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG - RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes - Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer - RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear - RoleId = $rbac.RoleDefinitionId - RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType - RoleDataRelated = $roleManageData - AssignmentType = 'indirect' - AssignmentInheritFrom = "$($rbac.RoleAssignmentIdentityDisplayname) ($($rbac.RoleAssignmentIdentityObjectId))" - GroupMembersCount = "$($grpHlpr.MembersAllCount) (Usr: $($grpHlpr.MembersUsersCount)$($CsvDelimiterOpposite) Grp: $($grpHlpr.MembersGroupsCount)$($CsvDelimiterOpposite) SP: $($grpHlpr.MembersServicePrincipalsCount))" - ObjectDisplayName = $grpMemberDisplayName - ObjectSignInName = $grpMemberSignInName - ObjectId = $grpMemberId - ObjectType = $identityTypeFull - RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment - RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear - RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner - RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP - RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments - }) - } - } - else { - $null = $script:rbacAll.Add([PSCustomObject]@{ - Level = $rbac.Level - RoleAssignmentId = $rbac.RoleAssignmentId - RoleAssignmentPIMRelated = $pim - RoleAssignmentPIMAssignmentType = $pimAssignmentType - RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart - RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd - CreatedBy = $rbac.RoleAssignmentCreatedBy - CreatedOn = $rbac.RoleAssignmentCreatedOn - UpdatedBy = $rbac.RoleAssignmentUpdatedBy - UpdatedOn = $rbac.RoleAssignmentUpdatedOn - MgId = $rbac.MgId - MgName = $rbac.MgName - MgParentId = $rbac.MgParentId - MgParentName = $rbac.MgParentName - SubscriptionId = $rbac.SubscriptionId - SubscriptionName = $rbac.Subscription - Scope = $scope - ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes - RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName - RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG - RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes - Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer - RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear - RoleId = $rbac.RoleDefinitionId - RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType - RoleDataRelated = $roleManageData - AssignmentType = 'indirect' - AssignmentInheritFrom = "$($rbac.RoleAssignmentIdentityDisplayname) ($($rbac.RoleAssignmentIdentityObjectId))" - GroupMembersCount = "$($grpHlpr.MembersAllCount) (Usr: $($grpHlpr.MembersUsersCount)$($CsvDelimiterOpposite) Grp: $($grpHlpr.MembersGroupsCount)$($CsvDelimiterOpposite) SP: $($grpHlpr.MembersServicePrincipalsCount))" - ObjectDisplayName = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" - ObjectSignInName = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" - ObjectId = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" - ObjectType = 'unresolved' - RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment - RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear - RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner - RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP - RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments - }) - } - } - - } - else { - - if ($rbac.RoleAssignmentIdentityObjectType -eq 'ServicePrincipal') { - $identityType = $htServicePrincipals.($rbac.RoleAssignmentIdentityObjectId).spTypeConcatinated - $identityTypeFull = $identityType - } - elseif ($rbac.RoleAssignmentIdentityObjectType -eq 'Unknown') { - $identityTypeFull = 'Unknown' - } - else { - #user - $identityType = $rbac.RoleAssignmentIdentityObjectType - $identityTypeFull = "$identityType $objectTypeUserType" - } - - $null = $script:rbacAll.Add([PSCustomObject]@{ - Level = $rbac.Level - RoleAssignmentId = $rbac.RoleAssignmentId - RoleAssignmentPIMRelated = $pim - RoleAssignmentPIMAssignmentType = $pimAssignmentType - RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart - RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd - CreatedBy = $rbac.RoleAssignmentCreatedBy - CreatedOn = $rbac.RoleAssignmentCreatedOn - UpdatedBy = $rbac.RoleAssignmentUpdatedBy - UpdatedOn = $rbac.RoleAssignmentUpdatedOn - MgId = $rbac.MgId - MgName = $rbac.MgName - MgParentId = $rbac.MgParentId - MgParentName = $rbac.MgParentName - SubscriptionId = $rbac.SubscriptionId - SubscriptionName = $rbac.Subscription - Scope = $scope - ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes - RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName - RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG - RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes - Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer - RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear - RoleId = $rbac.RoleDefinitionId - RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType - RoleDataRelated = $roleManageData - AssignmentType = 'direct' - AssignmentInheritFrom = '' - GroupMembersCount = '' - ObjectDisplayName = $rbac.RoleAssignmentIdentityDisplayname - ObjectSignInName = $rbac.RoleAssignmentIdentitySignInName - ObjectId = $rbac.RoleAssignmentIdentityObjectId - ObjectType = $identityTypeFull - RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment - RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear - RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner - RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP - RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments - }) - } - } - else { - - if ($rbac.RoleAssignmentIdentityObjectType -eq 'ServicePrincipal') { - $identityType = $htServicePrincipals.($rbac.RoleAssignmentIdentityObjectId).spTypeConcatinated - $identityTypeFull = $identityType - } - elseif ($rbac.RoleAssignmentIdentityObjectType -eq 'Unknown') { - $identityTypeFull = 'Unknown' - } - elseif ($rbac.RoleAssignmentIdentityObjectType -eq 'Group') { - $identityTypeFull = 'Group' - } - else { - #user - $identityType = $rbac.RoleAssignmentIdentityObjectType - $identityTypeFull = "$identityType $objectTypeUserType" - } - - #noaadgroupmemberresolve - $null = $script:rbacAll.Add([PSCustomObject]@{ - Level = $rbac.Level - RoleAssignmentId = $rbac.RoleAssignmentId - RoleAssignmentPIMRelated = $pim - RoleAssignmentPIMAssignmentType = $pimAssignmentType - RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart - RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd - CreatedBy = $rbac.RoleAssignmentCreatedBy - CreatedOn = $rbac.RoleAssignmentCreatedOn - UpdatedBy = $rbac.RoleAssignmentUpdatedBy - UpdatedOn = $rbac.RoleAssignmentUpdatedOn - MgId = $rbac.MgId - MgName = $rbac.MgName - MgParentId = $rbac.MgParentId - MgParentName = $rbac.MgParentName - SubscriptionId = $rbac.SubscriptionId - SubscriptionName = $rbac.Subscription - Scope = $scope - ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes - RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName - RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG - RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes - Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer - RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear - RoleId = $rbac.RoleDefinitionId - RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType - RoleDataRelated = $roleManageData - AssignmentType = 'direct' - AssignmentInheritFrom = '' - GroupMembersCount = '' - ObjectDisplayName = $rbac.RoleAssignmentIdentityDisplayname - ObjectSignInName = $rbac.RoleAssignmentIdentitySignInName - ObjectId = $rbac.RoleAssignmentIdentityObjectId - ObjectType = $identityTypeFull - RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment - RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear - RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner - RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP - RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments - }) - } - } - #endregion createRBACAll - - #region PIMEligible - if (-not $NoPIMEligibility) { - $startPIMEnrichment = Get-Date - Write-Host ' Processing PIMEnrichment' - $PIMEligibleEnriched = [System.Collections.ArrayList]@() - #$tfCountCnt = 0 - foreach ($PIMEligible in $arrayPIMEligible) { - #$tfCountCnt++ - if ($PIMEligible.RoleType -eq 'BuiltInRole') { - $roleName = "$($PIMEligible.RoleName)" - } - else { - $roleName = $PIMEligible.RoleName - } - $null = $PIMEligibleEnriched.Add([PSCustomObject]@{ - Scope = $PIMEligible.ScopeType - ScopeId = $PIMEligible.ScopeId - ScopeName = $PIMEligible.ScopeDisplayName - ManagementGroupId = $PIMEligible.ManagementGroupId - ManagementGroupDisplayName = $PIMEligible.ManagementGroupDisplayName - SubscriptionId = $PIMEligible.SubscriptionId - SubscriptionDisplayName = $PIMEligible.SubscriptionDisplayName - MgPath = $PIMEligible.MgPath -join '/' - MgLevel = $PIMEligible.MgLevel - Role = $roleName - RoleClear = $PIMEligible.RoleName - RoleId = $PIMEligible.RoleId - RoleIdGuid = $PIMEligible.RoleIdGuid - RoleType = $PIMEligible.RoleType - IdentityObjectId = $PIMEligible.IdentityObjectId - IdentityDisplayName = $PIMEligible.IdentityDisplayName - IdentitySignInName = $PIMEligible.IdentityPrincipalName - IdentityType = $PIMEligible.IdentityType - IdentityApplicability = 'direct' - AppliesThrough = '' - PIMEligibilityId = $PIMEligible.PIMId - PIMEligibility = $PIMEligible.PIMInheritance - PIMEligibilityInheritedFrom = $PIMEligible.PIMInheritedFrom - PIMEligibilityInheritedFromClear = $PIMEligible.PIMInheritedFromClear - PIMEligibilityStartDateTime = [string]$PIMEligible.PIMStartDateTime - PIMEligibilityEndDateTime = [string]$PIMEligible.PIMEndDateTime - }) - - if (-not $NoAADGroupsResolveMembers) { - if ($PIMEligible.IdentityType -eq 'Group') { - if ($htAADGroupsDetails.($PIMEligible.IdentityObjectId)) { - foreach ($groupMemberUser in $htAADGroupsDetails.($PIMEligible.IdentityObjectId).MembersUsers) { - #$tfCountCnt++ - $null = $PIMEligibleEnriched.Add([PSCustomObject]@{ - Scope = $PIMEligible.ScopeType - ScopeId = $PIMEligible.ScopeId - ScopeName = $PIMEligible.ScopeDisplayName - ManagementGroupId = $PIMEligible.ManagementGroupId - ManagementGroupDisplayName = $PIMEligible.ManagementGroupDisplayName - SubscriptionId = $PIMEligible.SubscriptionId - SubscriptionDisplayName = $PIMEligible.SubscriptionDisplayName - MgPath = $PIMEligible.MgPath -join '/' - MgLevel = $PIMEligible.MgLevel - Role = $roleName - RoleClear = $PIMEligible.RoleName - RoleId = $PIMEligible.RoleId - RoleIdGuid = $PIMEligible.RoleIdGuid - RoleType = $PIMEligible.RoleType - IdentityObjectId = $groupMemberUser.id - IdentityDisplayName = $groupMemberUser.displayName - IdentitySignInName = $groupMemberUser.userPrincipalName - IdentityType = "User $($groupMemberUser.userType)" - IdentityApplicability = 'nested' - AppliesThrough = "$($PIMEligible.IdentityDisplayName) ($($PIMEligible.IdentityObjectId))" - PIMEligibilityId = $PIMEligible.PIMId - PIMEligibility = $PIMEligible.PIMInheritance - PIMEligibilityInheritedFrom = $PIMEligible.PIMInheritedFrom - PIMEligibilityInheritedFromClear = $PIMEligible.PIMInheritedFromClear - PIMEligibilityStartDateTime = [string]$PIMEligible.PIMStartDateTime - PIMEligibilityEndDateTime = [string]$PIMEligible.PIMEndDateTime - }) - } - } - else { - Write-Host "!! Unexpected: Group $($PIMEligible.IdentityDisplayName) ($($PIMEligible.IdentityObjectId)) not found in `$htAADGroupsDetails - please report back!" - } - } - } - } - $endPIMEnrichment = Get-Date - Write-Host " PIMEnrichment duration: $((New-TimeSpan -Start $startPIMEnrichment -End $endPIMEnrichment).TotalMinutes) minutes ($((New-TimeSpan -Start $startPIMEnrichment -End $endPIMEnrichment).TotalSeconds) seconds)" - - if (-not $NoPIMEligibilityIntegrationRoleAssignmentsAll) { - $startPIMEnrichmentToRBACAll = Get-Date - Write-Host ' Processing PIMEnrichment to RBACAll' - foreach ($PIMEligibleRoleAssignment in $PIMEligibleEnriched) { - if ($PIMEligibleRoleAssignment.PIMEligibility -eq 'Inherited') { - $scope = "inherited $($PIMEligibleRoleAssignment.PIMEligibilityInheritedFromClear)" - } - else { - $scope = "thisScope $($PIMEligibleRoleAssignment.Scope)" - } - - if (-not [string]::IsNullOrEmpty($htCacheDefinitionsRole.($PIMEligibleRoleAssignment.RoleId).RoleDataActions) -or -not [string]::IsNullOrEmpty($htCacheDefinitionsRole.($PIMEligibleRoleAssignment.RoleId).RoleNotDataActions)) { - $roleManageData = 'true' - } - else { - $roleManageData = 'false' - } - - $roleCanDoRoleAssignments = $false - if ($htCacheDefinitionsRole.($PIMEligibleRoleAssignment.RoleId).RoleCanDoRoleAssignments) { - $roleCanDoRoleAssignments = 'true' - } - - $null = $script:rbacAll.Add([PSCustomObject]@{ - Level = $PIMEligibleRoleAssignment.MgLevel - RoleAssignmentId = '' - RoleAssignmentPIMRelated = $true - RoleAssignmentPIMAssignmentType = 'Eligible' - RoleAssignmentPIMAssignmentSlotStart = $PIMEligibleRoleAssignment.PIMEligibilityStartDateTime - RoleAssignmentPIMAssignmentSlotEnd = $PIMEligibleRoleAssignment.PIMEligibilityEndDateTime - CreatedBy = '' - CreatedOn = '' - UpdatedBy = $rbac.RoleAssignmentUpdatedBy - UpdatedOn = $rbac.RoleAssignmentUpdatedOn - MgId = $PIMEligibleRoleAssignment.ManagementGroupId - MgName = $PIMEligibleRoleAssignment.ManagementGroupDisplayName - MgParentId = '' #check - MgParentName = '' #check - SubscriptionId = $PIMEligibleRoleAssignment.SubscriptionId - SubscriptionName = $PIMEligibleRoleAssignment.SubscriptionDisplayName - Scope = $scope - ScopeTenOrMgOrSubOrRGOrRes = $PIMEligibleRoleAssignment.Scope - RoleAssignmentScopeName = $PIMEligibleRoleAssignment.Scope - RoleAssignmentScopeRG = '' - RoleAssignmentScopeRes = '' - Role = $PIMEligibleRoleAssignment.Role - RoleClear = $PIMEligibleRoleAssignment.RoleClear - RoleId = $PIMEligibleRoleAssignment.RoleIdGuid - RoleType = $PIMEligibleRoleAssignment.RoleType - RoleDataRelated = $roleManageData #check - AssignmentType = $PIMEligibleRoleAssignment.IdentityApplicability - AssignmentInheritFrom = $PIMEligibleRoleAssignment.AppliesThrough - GroupMembersCount = '' - ObjectDisplayName = $PIMEligibleRoleAssignment.IdentityDisplayName - ObjectSignInName = $PIMEligibleRoleAssignment.IdentitySignInName - ObjectId = $PIMEligibleRoleAssignment.IdentityObjectId - ObjectType = $PIMEligibleRoleAssignment.IdentityType - RbacRelatedPolicyAssignment = '' - RbacRelatedPolicyAssignmentClear = '' - RoleSecurityCustomRoleOwner = '' #check $rbac.RoleSecurityCustomRoleOwner - RoleSecurityOwnerAssignmentSP = '' #check $rbac.RoleSecurityOwnerAssignmentSP - RoleCanDoRoleAssignments = $roleCanDoRoleAssignments - }) - } - $endPIMEnrichmentToRBACAll = Get-Date - Write-Host " PIMEnrichment to RBACAll duration: $((New-TimeSpan -Start $startPIMEnrichmentToRBACAll -End $endPIMEnrichmentToRBACAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startPIMEnrichmentToRBACAll -End $endPIMEnrichmentToRBACAll).TotalSeconds) seconds)" - } - } - #endregion PIMEligible - - Write-Host ' Processing unresoved Identities (createdBy)' - $startUnResolvedIdentitiesCreatedBy = Get-Date - #prep prepUnresoledIdentities - #region identitiesThatCreatedRoleAssignmentsButDontHaveARoleAssignmentThemselve - $script:htIdentitiesWithRoleAssignmentsUnique = @{} - $identitiesWithRoleAssignmentsUnique = $rbacAll.where( { $_.ObjectType -ne 'Unknown' } ) | Sort-Object -Property ObjectId -Unique | Select-Object ObjectType, ObjectDisplayName, ObjectSignInName, ObjectId - foreach ($identityWithRoleAssignment in $identitiesWithRoleAssignmentsUnique | Sort-Object -Property objectType) { - - if (-not $htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId)) { - $script:htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId) = @{} - - $arr = @() - $ht = [ordered]@{} - $identityWithRoleAssignment.psobject.properties | ForEach-Object { - if ($_.Value) { - $value = $_.Value - } - else { - $value = 'n/a' - } - $arr += "$($_.Name): $value" - $ht.($_.Name) = $value - } - - $script:htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId).details = $arr -join "$CsvDelimiterOpposite " - $script:htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId).detailsJson = $ht - } - } - #endregion identitiesThatCreatedRoleAssignmentsButDontHaveARoleAssignmentThemselve - - #enrich rbacAll with createdBy and UpdatedBy identity information - #region enrichrbacAll - $htNonResolvedIdentities = @{} - foreach ($rbac in $rbacAll) { - $createdBy = $rbac.createdBy - if (-not [string]::IsNullOrEmpty($createdBy)) { - if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { - $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details - $rbac.CreatedBy = $createdBy - } - else { - if (-not $htNonResolvedIdentities.($rbac.createdBy)) { - $htNonResolvedIdentities.($rbac.createdBy) = @{} - } - } - } - - $updatedBy = $rbac.updatedBy - if (-not [string]::IsNullOrEmpty($updatedBy)) { - if ($htIdentitiesWithRoleAssignmentsUnique.($updatedBy)) { - $updatedBy = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).details - $rbac.UpdatedBy = $updatedBy - } - else { - if (-not $htNonResolvedIdentities.($rbac.updatedBy)) { - $htNonResolvedIdentities.($rbac.updatedBy) = @{} - } - } - } - } - #endregion enrichrbacAll - - #region nonResolvedIdentities - $htNonResolvedIdentitiesCount = $htNonResolvedIdentities.Count - if ($htNonResolvedIdentitiesCount -gt 0) { - Write-Host " $htNonResolvedIdentitiesCount unresolved identities that created a RBAC Role assignment (createdBy)" - $arrayUnresolvedIdentities = @() - $arrayUnresolvedIdentities = foreach ($unresolvedIdentity in $htNonResolvedIdentities.keys) { - if (-not [string]::IsNullOrEmpty($unresolvedIdentity)) { - $unresolvedIdentity - } - } - $arrayUnresolvedIdentitiesCount = $arrayUnresolvedIdentities.Count - Write-Host " $arrayUnresolvedIdentitiesCount unresolved identities that have a value" - if ($arrayUnresolvedIdentitiesCount -gt 0) { - - $counterBatch = [PSCustomObject] @{ Value = 0 } - $batchSize = 1000 - $ObjectBatch = $arrayUnresolvedIdentities | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } - $ObjectBatchCount = ($ObjectBatch | Measure-Object).Count - $batchCnt = 0 - - $script:htResolvedIdentities = @{} - - foreach ($batch in $ObjectBatch) { - $batchCnt++ - - $nonResolvedIdentitiesToCheck = '"{0}"' -f ($batch.Group.where({ testGuid $_ }) -join '","') - Write-Host " IdentitiesToCheck: Batch #$batchCnt/$($ObjectBatchCount) ($(($batch.Group).Count))" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/directoryObjects/getByIds" - $method = 'POST' - $body = @" - { - "ids":[$($nonResolvedIdentitiesToCheck)] - } -"@ - - function resolveIdentitiesRBAC($currentTask) { - $resolvedIdentities = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask - $resolvedIdentitiesCount = $resolvedIdentities.Count - Write-Host " $resolvedIdentitiesCount identities resolved" - if ($resolvedIdentitiesCount -gt 0) { - - foreach ($resolvedIdentity in $resolvedIdentities) { - - if (-not $htResolvedIdentities.($resolvedIdentity.id)) { - - $script:htResolvedIdentities.($resolvedIdentity.id) = @{} - if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.servicePrincipal' -or $resolvedIdentity.'@odata.type' -eq '#microsoft.graph.user') { - if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { - if ($resolvedIdentity.servicePrincipalType -eq 'ManagedIdentity') { - $miType = 'unknown' - foreach ($altName in $resolvedIdentity.alternativeNames) { - if ($altName -like 'isExplicit=*') { - $splitAltName = $altName.split('=') - if ($splitAltName[1] -eq 'true') { - $miType = 'Usr' - } - if ($splitAltName[1] -eq 'false') { - $miType = 'Sys' - } - } - } - $sptype = "MI $miType" - $custObjectType = "ObjectType: SP $sptype, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (r)" - $ht = @{} - $ht.'ObjectType' = "SP $sptype" - $ht.'ObjectDisplayName' = $($resolvedIdentity.displayName) - $ht.'ObjectSignInName' = 'n/a' - $ht.'ObjectId' = $resolvedIdentity.id - } - else { - if ($resolvedIdentity.servicePrincipalType -eq 'Application') { - $sptype = 'App' - if ($resolvedIdentity.appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Tenant.Id) { - $custObjectType = "ObjectType: SP $sptype INT, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (r)" - $ht = @{} - $ht.'ObjectType' = "SP $sptype INT" - $ht.'ObjectDisplayName' = $($resolvedIdentity.displayName) - $ht.'ObjectSignInName' = 'n/a' - $ht.'ObjectId' = $resolvedIdentity.id - } - else { - $custObjectType = "ObjectType: SP $sptype EXT, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (r)" - $ht = @{} - $ht.'ObjectType' = "SP $sptype EXT" - $ht.'ObjectDisplayName' = $($resolvedIdentity.displayName) - $ht.'ObjectSignInName' = 'n/a' - $ht.'ObjectId' = $resolvedIdentity.id - } - } - else { - Write-Host "* * * Unexpected IdentityType $($resolvedIdentity.servicePrincipalType)" - } - } - $script:htResolvedIdentities.($resolvedIdentity.id).custObjectType = $custObjectType - $script:htResolvedIdentities.($resolvedIdentity.id).obj = $resolvedIdentity - } - - if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.user') { - if ($htParamteters.DoNotShowRoleAssignmentsUserData) { - $hlpObjectDisplayName = 'scrubbed' - $hlpObjectSigninName = 'scrubbed' - } - else { - $hlpObjectDisplayName = $resolvedIdentity.displayName - $hlpObjectSigninName = $resolvedIdentity.userPrincipalName - } - $custObjectType = "ObjectType: User, ObjectDisplayName: $hlpObjectDisplayName, ObjectSignInName: $hlpObjectSigninName, ObjectId: $($resolvedIdentity.id) (r)" - $ht = @{} - $ht.'ObjectType' = 'User' - $ht.'ObjectDisplayName' = $hlpObjectDisplayName - $ht.'ObjectSignInName' = $hlpObjectSigninName - $ht.'ObjectId' = $resolvedIdentity.id - - $script:htResolvedIdentities.($resolvedIdentity.id).custObjectType = $custObjectType - $script:htResolvedIdentities.($resolvedIdentity.id).obj = $resolvedIdentity - } - if (-not $htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id)) { - $script:htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id) = @{} - $script:htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id).details = $custObjectType - $script:htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id).detailsJson = $ht - } - } - - if ($resolvedIdentity.'@odata.type' -ne '#microsoft.graph.user' -and $resolvedIdentity.'@odata.type' -ne '#microsoft.graph.servicePrincipal') { - Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by Azure Governance Visualizer - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow - } - } - } - } - } - resolveIdentitiesRBAC -currentTask ' resolveObjectbyId RoleAssignment' - } - - foreach ($rbac in $rbacAll.where( { $_.CreatedBy -notlike 'ObjectType*' -or $_.UpdatedBy -notlike 'ObjectType*' })) { - if ($rbac.CreatedBy -notlike 'ObjectType*') { - if ($htResolvedIdentities.($rbac.CreatedBy)) { - $rbac.CreatedBy = $htResolvedIdentities.($rbac.CreatedBy).custObjectType - } - else { - if ($rbac.RoleAssignmentPIMAssignmentType -eq 'Eligible') { - $rbac.CreatedBy = '' - } - else { - if ([string]::IsNullOrEmpty($rbac.CreatedBy)) { - $rbac.CreatedBy = 'IsNullOrEmpty' - } - else { - $rbac.CreatedBy = "$($rbac.CreatedBy)" - } - } - } - } - if ($rbac.UpdatedBy -notlike 'ObjectType*') { - if ($htResolvedIdentities.($rbac.UpdatedBy)) { - $rbac.UpdatedBy = $htResolvedIdentities.($rbac.UpdatedBy).custObjectType - } - else { - if ($rbac.RoleAssignmentPIMAssignmentType -eq 'Eligible') { - $rbac.UpdatedBy = '' - } - else { - if ([string]::IsNullOrEmpty($rbac.UpdatedBy)) { - $rbac.UpdatedBy = 'IsNullOrEmpty' - } - else { - $rbac.UpdatedBy = "$($rbac.UpdatedBy)" - } - } - } - } - } - } - } - $endUnResolvedIdentitiesCreatedBy = Get-Date - Write-Host " UnresolvedIdentities (createdBy) duration: $((New-TimeSpan -Start $startUnResolvedIdentitiesCreatedBy -End $endUnResolvedIdentitiesCreatedBy).TotalMinutes) minutes ($((New-TimeSpan -Start $startUnResolvedIdentitiesCreatedBy -End $endUnResolvedIdentitiesCreatedBy).TotalSeconds) seconds)" - #endregion nonResolvedIdentities - - $startRBACAllGrouping = Get-Date - $script:rbacAllGroupedBySubscription = $rbacAll | Group-Object -Property SubscriptionId - $script:rbacAllGroupedByManagementGroup = $rbacAll | Group-Object -Property MgId - $endRBACAllGrouping = Get-Date - Write-Host " RBACAll Grouping duration: $((New-TimeSpan -Start $startRBACAllGrouping -End $endRBACAllGrouping).TotalMinutes) minutes ($((New-TimeSpan -Start $startRBACAllGrouping -End $endRBACAllGrouping).TotalSeconds) seconds)" - $endCreateRBACAll = Get-Date - Write-Host " CreateRBACAll duration: $((New-TimeSpan -Start $startCreateRBACAll -End $endCreateRBACAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startCreateRBACAll -End $endCreateRBACAll).TotalSeconds) seconds)" - #endregion tenantSummaryPre - - showMemoryUsage - - #region tenantSummaryPolicy - $htmlTenantSummary = [System.Text.StringBuilder]::new() - [void]$htmlTenantSummary.AppendLine(@' - -
- Anything which can help you learn Azure Policy GitHub
-'@) - - #region SUMMARYcustompolicies - $startCustPolLoop = Get-Date - Write-Host ' processing TenantSummary Custom Policy definitions' - - $script:customPoliciesDetailed = [System.Collections.ArrayList]@() - $script:tenantPoliciesDetailed = [System.Collections.ArrayList]@() - foreach ($tenantPolicy in (($htCacheDefinitionsPolicy).Values | Sort-Object @{Expression = { $_.DisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { - - #uniqueAssignments - $policyUniqueAssignments = $null - if ($htPolicyWithAssignmentsBase.($tenantPolicy.PolicyDefinitionId)) { - $policyUniqueAssignments = $htPolicyWithAssignmentsBase.($tenantPolicy.PolicyDefinitionId).Assignments | Sort-Object - $policyUniqueAssignmentsCount = ($policyUniqueAssignments).count - } - else { - $policyUniqueAssignmentsCount = 0 - } - - $uniqueAssignments = $null - if ($policyUniqueAssignmentsCount -gt 0) { - $policyUniqueAssignmentsList = "($($policyUniqueAssignments -join "$CsvDelimiterOpposite "))" - $uniqueAssignments = "$policyUniqueAssignmentsCount $policyUniqueAssignmentsList" - } - else { - $uniqueAssignments = $policyUniqueAssignmentsCount - } - - #PolicyUsedInPolicySet - $usedInPolicySet4JSON = $null - $usedInPolicySet = 0 - $usedInPolicySet4CSV = '' - $usedInPolicySetCount = 0 - if (($htPoliciesUsedInPolicySets).($tenantPolicy.PolicyDefinitionId)) { - $hlpPolicySetUsed = ($htPoliciesUsedInPolicySets).($tenantPolicy.PolicyDefinitionId) - $usedInPolicySet4JSON = $hlpPolicySetUsed.PolicySetIdOnly | Sort-Object - $usedInPolicySet = "$(($hlpPolicySetUsed.PolicySet | Sort-Object) -join "$CsvDelimiterOpposite ")" - $usedInPolicySet4CSV = "$(($hlpPolicySetUsed.PolicySet4CSV | Sort-Object) -join "$CsvDelimiterOpposite ")" - $usedInPolicySetCount = ($hlpPolicySetUsed.PolicySet).Count - } - - #policyEffect - if ($tenantPolicy.effectDefaultValue -ne 'n/a') { - $effect = "Default: $($tenantPolicy.effectDefaultValue); Allowed: $($tenantPolicy.effectAllowedValue)" - } - elseif ($tenantPolicy.effectFixedValue -ne 'n/a') { - $effect = "Fixed: $($tenantPolicy.effectFixedValue)" - } - else { - $effect = 'n/a' - } - - if (($tenantPolicy.RoleDefinitionIds) -ne 'n/a') { - $policyRoleDefinitionsArray = @() - $policyRoleDefinitionsArray = foreach ($roleDefinitionId in $tenantPolicy.RoleDefinitionIds | Sort-Object) { - if (($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').LinkToAzAdvertizer) { - ($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').LinkToAzAdvertizer - } - else { - ($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').Name -replace '<', '<' -replace '>', '>' - } - } - $policyRoleDefinitionsClearArray = @() - $policyRoleDefinitionsClearArray = foreach ($roleDefinitionId in $tenantPolicy.RoleDefinitionIds | Sort-Object) { - ($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').Name - } - $policyRoleDefinitions = $policyRoleDefinitionsArray -join "$CsvDelimiterOpposite " - $policyRoleDefinitionsClear = $policyRoleDefinitionsClearArray -join "$CsvDelimiterOpposite " - } - else { - $policyRoleDefinitions = 'n/a' - $policyRoleDefinitionsClear = 'n/a' - } - - # if ($tenantPolicy.Json.properties.metadata.version) { - # $policyVersion = $tenantPolicy.Json.properties.metadata.version - # } - # else { - # $policyVersion = 'n/a' - # } - - if ($tenantPolicy.Type -eq 'Custom') { - - $createdOn = '' - $createdBy = '' - $createdByJson = '' - $updatedOn = '' - $updatedBy = '' - $updatedByJson = '' - if ($tenantPolicy.Json.properties.metadata.createdOn) { - $createdOn = $tenantPolicy.Json.properties.metadata.createdOn - } - if ($tenantPolicy.Json.properties.metadata.createdBy) { - $createdBy = $tenantPolicy.Json.properties.metadata.createdBy - $createdByJson = $createdBy - if ($createdBy -ne 'n/a') { - if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { - $createdByJson = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).detailsJson - $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details - - } - } - } - if ($tenantPolicy.Json.properties.metadata.updatedOn) { - $updatedOn = $tenantPolicy.Json.properties.metadata.updatedOn - } - if ($tenantPolicy.Json.properties.metadata.updatedBy) { - $updatedBy = $tenantPolicy.Json.properties.metadata.updatedBy - $updatedByJson = $updatedBy - if ($updatedBy -ne 'n/a') { - if ($htIdentitiesWithRoleAssignmentsUnique.($updatedBy)) { - $updatedByJson = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).detailsJson - $updatedBy = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).details - - } - } - } - - $null = $script:customPoliciesDetailed.Add([PSCustomObject]@{ - Type = 'Custom' - ScopeMGLevel = $tenantPolicy.ScopeMGLevel - Scope = $tenantPolicy.ScopeMgSub - ScopeId = $tenantPolicy.ScopeId - PolicyDisplayName = $tenantPolicy.DisplayName - PolicyDefinitionName = $tenantPolicy.Name - PolicyDefinitionId = $tenantPolicy.PolicyDefinitionId - PolicyVersion = $tenantPolicy.Version - PolicyEffect = $effect - PolicyCategory = $tenantPolicy.Category - RoleDefinitions = $policyRoleDefinitions - RoleDefinitionsClear = $policyRoleDefinitionsClear - UniqueAssignments = $uniqueAssignments - UsedInPolicySetsCount = $usedInPolicySetCount - UsedInPolicySets = $usedInPolicySet - UsedInPolicySet4CSV = $usedInPolicySet4CSV - CreatedOn = $createdOn - CreatedBy = $createdBy - UpdatedOn = $updatedOn - UpdatedBy = $updatedBy - ALZ = $tenantPolicy.ALZ - ALZState = $tenantPolicy.ALZState - ALZLatestVer = $tenantPolicy.ALZLatestVer - ALZIdentificationLevel = $tenantPolicy.ALZIdentificationLevel - ALZPolicyName = $tenantPolicy.ALZPolicyName - #Json = [string]($tenantPolicy.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) - }) - - $null = $script:tenantPoliciesDetailed.Add([PSCustomObject]@{ - Type = 'Custom' - ScopeMGLevel = $tenantPolicy.ScopeMGLevel - Scope = $tenantPolicy.ScopeMgSub - ScopeId = $tenantPolicy.ScopeId - PolicyDisplayName = $tenantPolicy.DisplayName - PolicyDefinitionName = $tenantPolicy.Name - PolicyDefinitionId = $tenantPolicy.PolicyDefinitionId - PolicyVersion = $tenantPolicy.Version - PolicyEffect = $effect - PolicyCategory = $tenantPolicy.Category - UniqueAssignmentsCount = $policyUniqueAssignmentsCount - UniqueAssignments = $policyUniqueAssignments - UsedInPolicySetsCount = $usedInPolicySetCount - UsedInPolicySets = $usedInPolicySet - UsedInPolicySet4CSV = $usedInPolicySet4CSV - UsedInPolicySet4JSON = $usedInPolicySet4JSON - CreatedOn = $createdOn - CreatedBy = $createdBy - CreatedByJson = $createdByJson - UpdatedOn = $updatedOn - UpdatedBy = $updatedBy - UpdatedByJson = $updatedByJson - #Json = [string]($tenantPolicy.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) - Json = $tenantPolicy.Json - ALZ = $tenantPolicy.ALZ - ALZState = $tenantPolicy.ALZState - ALZLatestVer = $tenantPolicy.ALZLatestVer - ALZIdentificationLevel = $tenantPolicy.ALZIdentificationLevel - ALZPolicyName = $tenantPolicy.ALZPolicyName - }) - } - else { - $null = $script:tenantPoliciesDetailed.Add([PSCustomObject]@{ - Type = $tenantPolicy.Type - ScopeMGLevel = $null - Scope = $null - ScopeId = $null - PolicyDisplayName = $tenantPolicy.DisplayName - PolicyDefinitionName = $tenantPolicy.Name - PolicyDefinitionId = $tenantPolicy.PolicyDefinitionId - PolicyVersion = $tenantPolicy.Version - PolicyEffect = $effect - PolicyCategory = $tenantPolicy.Category - UniqueAssignmentsCount = $policyUniqueAssignmentsCount - UniqueAssignments = $policyUniqueAssignments - UsedInPolicySetsCount = $usedInPolicySetCount - UsedInPolicySets = $usedInPolicySet - UsedInPolicySet4CSV = $usedInPolicySet4CSV - UsedInPolicySet4JSON = $usedInPolicySet4JSON - CreatedOn = $null - CreatedBy = $null - CreatedByJson = $null - UpdatedOn = $null - UpdatedBy = $null - UpdatedByJson = $null - #Json = [string]($tenantPolicy.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) - Json = $tenantPolicy.Json - ALZ = $tenantPolicy.ALZ - ALZState = $tenantPolicy.ALZState - ALZLatestVer = $tenantPolicy.ALZLatestVer - ALZIdentificationLevel = $tenantPolicy.ALZIdentificationLevel - ALZPolicyName = $tenantPolicy.ALZPolicyName - }) - } - } - - if (-not $NoCsvExport) { - $csvFilename = "$($filename)_PolicyDefinitions" - Write-Host " Exporting PolicyDefinitions CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" - $tenantPoliciesDetailed | Sort-Object -Property Type, Scope, PolicyDefinitionId | Select-Object -ExcludeProperty UniqueAssignments, UsedInPolicySets, UsedInPolicySet4JSON, CreatedByJson, UpdatedByJson, Json | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation - } - - if ($getMgParentName -eq 'Tenant Root') { - - if ($tenantCustomPoliciesCount -gt 0) { - $tfCount = $tenantCustomPoliciesCount - $htmlTableId = 'TenantSummary_customPolicies' - [void]$htmlTenantSummary.AppendLine(@" - -
- Download CSV semicolon | comma -
- $($diagnosticsFinding.ResourceType) + $($diagnosticsFinding.ResourceType) $($diagnosticsFinding.ResourceTypeCount) @@ -24129,24 +24129,24 @@ extensions: [{ name: 'sort' }] #policySets if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { [void]$htmlTenantSummary.AppendLine(@" -

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

"@) } @@ -24166,7 +24166,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma @@ -24239,7 +24239,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyAssignments @@ -24253,7 +24253,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24326,7 +24326,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicyScope @@ -24340,7 +24340,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24413,7 +24413,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsPolicySetScope @@ -24428,7 +24428,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -24501,7 +24501,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYMgsapproachingLimitsRoleAssignment @@ -24522,7 +24522,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Resource Group Limit docs
+ Azure Subscription Resource Group Limit docs
Download CSV semicolon | comma
@@ -24596,7 +24596,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

+ $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

"@) } #endregion SUMMARYSubsapproachingLimitsResourceGroups @@ -24610,7 +24610,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Subscription Tag Limit docs
+ Azure Subscription Tag Limit docs
Download CSV semicolon | comma
@@ -24683,7 +24683,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsSubscriptionTags @@ -24697,7 +24697,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24770,7 +24770,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyAssignments @@ -24784,7 +24784,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24857,7 +24857,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicyScope @@ -24871,7 +24871,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure Policy Limits docs
+ Azure Policy Limits docs
Download CSV semicolon | comma
@@ -24944,7 +24944,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYSubsapproachingLimitsPolicySetScope @@ -24961,7 +24961,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
- Azure RBAC Limits docs
+ Azure RBAC Limits docs
Download CSV semicolon | comma
@@ -25034,7 +25034,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

+ $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

"@) } #endregion SUMMARYSubsapproachingLimitsRoleAssignment @@ -25275,7 +25275,7 @@ tf.init();}} } } else { - #https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.escape + #https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.regex.escape $s1 = $altName -replace '.*/providers/' $rm = $s1 -replace '.*/' $resourceType = $s1 -replace "/$([System.Text.RegularExpressions.Regex]::Escape($rm))" @@ -28143,7 +28143,7 @@ function runInfo { } if (-not $NoAADGroupsResolveMembers) { - Write-Host " AAD Groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving AAD Group memberships" -ForegroundColor Yellow + Write-Host " Microsoft Entra groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving group memberships" -ForegroundColor Yellow $script:paramsUsed += 'NoAADGroupsResolveMembers: false ' if ($AADGroupMembersLimit -eq 500) { Write-Host " AADGroupMembersLimit = $AADGroupMembersLimit" -ForegroundColor Yellow @@ -28155,7 +28155,7 @@ function runInfo { } } else { - Write-Host " AAD Groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green + Write-Host " Microsoft Entra groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green $script:paramsUsed += 'NoAADGroupsResolveMembers: true ' } @@ -29265,7 +29265,7 @@ function dataCollectionDefenderPlans { ) $currentTask = "Getting Microsoft Defender for Cloud plans for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/defenderforcloud/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/pricings?api-version=2018-06-01" $method = 'GET' $defenderPlansResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29350,7 +29350,7 @@ function dataCollectionDefenderEmailContacts { ) $currentTask = "Getting Microsoft Defender for Cloud Email contacts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + #https://learn.microsoft.com/rest/api/defenderforcloud/security-contacts $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview" $method = 'GET' $defenderSecurityContactsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -listenOn 'Content' -currentTask $currentTask -caller 'CustomDataCollection' @@ -29447,7 +29447,6 @@ function dataCollectionVNets { ) $currentTask = "Getting Virtual Networks for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/virtualNetworks?api-version=2022-05-01" $method = 'GET' $networkResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -29474,7 +29473,6 @@ function dataCollectionPrivateEndpoints { ) $currentTask = "Getting Private Endpoints for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" - #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/privateEndpoints?api-version=2022-05-01" $method = 'GET' $privateEndpointsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -unhandledErrorAction Continue @@ -33674,7 +33672,7 @@ function processScopeInsights($mgChild, $mgChildOf) { "@ if ($mgId -eq $defaultManagementGroupId) { $script:html += @' -
+ '@ } $script:html += @" diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index be63985a..7a212338 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -62,7 +62,7 @@ use this parameter if Azure Consumption data should not be exported (CSV) .PARAMETER ThrottleLimit - Leveraging PowerShell Core´s parallel capability you can define the ThrottleLimit (default=5) + Leveraging PowerShell Core's parallel capability you can define the ThrottleLimit (default=5) .PARAMETER DoTranscript Log the console output @@ -236,7 +236,7 @@ Define for which time period (days) Azure Consumption data should be gathered; e.g. 14 days; default is 1 day PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -AzureConsumptionPeriod 14 - Define the number of script blocks running in parallel. Leveraging PowerShell Core´s parallel capability you can define the ThrottleLimit (default=5) + Define the number of script blocks running in parallel. Leveraging PowerShell Core's parallel capability you can define the ThrottleLimit (default=5) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -ThrottleLimit 10 Define if you want to log the console output @@ -279,7 +279,7 @@ If the parameter switch is true then the following parameters will be set: -PolicyAtScopeOnly $true -RBACAtScopeOnly $true - - NoResourceProvidersAtAll $true + -NoResourceProvidersAtAll $true -NoScopeInsights $true PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -LargeTenant @@ -302,7 +302,7 @@ Note if you use parameter -LargeTenant then parameter -NoScopeInsights will be set to true PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoScopeInsights - Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolved +Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolved PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -AADGroupMembersLimit 750 Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants) @@ -365,7 +365,7 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.3.7', + $ProductVersion = '6.3.71', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -645,33143 +645,69 @@ if ($ManagementGroupId -match ' ') { } #region Functions -function addHtParameters { - Write-Host 'Add Azure Governance Visualizer htParameters' - if ($LargeTenant -eq $true) { - $script:NoScopeInsights = $true - $NoResourceProvidersAtAll = $true - $PolicyAtScopeOnly = $true - $RBACAtScopeOnly = $true - } - - if ($ManagementGroupsOnly) { - $script:NoSingleSubscriptionOutput = $true - } - - if ($HierarchyMapOnly) { - $NoJsonExport = $true - } - - $script:azAPICallConf['htParameters'] += [ordered]@{ - DoAzureConsumption = [bool]$DoAzureConsumption - DoNotIncludeResourceGroupsOnPolicy = [bool]$DoNotIncludeResourceGroupsOnPolicy - DoNotIncludeResourceGroupsAndResourcesOnRBAC = [bool]$DoNotIncludeResourceGroupsAndResourcesOnRBAC - DoNotShowRoleAssignmentsUserData = [bool]$DoNotShowRoleAssignmentsUserData - HierarchyMapOnly = [bool]$HierarchyMapOnly - LargeTenant = [bool]$LargeTenant - ManagementGroupsOnly = [bool]$ManagementGroupsOnly - NoJsonExport = [bool]$NoJsonExport - NoMDfCSecureScore = [bool]$NoMDfCSecureScore - NoResourceProvidersDetailed = [bool]$NoResourceProvidersDetailed - NoResourceProvidersAtAll = [bool]$NoResourceProvidersAtAll - NoPolicyComplianceStates = [bool]$NoPolicyComplianceStates - NoResources = [bool]$NoResources - ProductVersion = $ProductVersion - PolicyAtScopeOnly = [bool]$PolicyAtScopeOnly - RBACAtScopeOnly = [bool]$RBACAtScopeOnly - DoPSRule = [bool]$DoPSRule - PSRuleFailedOnly = [bool]$PSRuleFailedOnly - NoALZPolicyVersionChecker = [bool]$NoALZPolicyVersionChecker - NoStorageAccountAccessAnalysis = [bool]$NoStorageAccountAccessAnalysis - GitHubActionsOIDC = [bool]$GitHubActionsOIDC - NoNetwork = [bool]$NoNetwork - ThrottleLimit = $ThrottleLimit - } - Write-Host 'htParameters:' - $azAPICallConf['htParameters'] | Format-Table -AutoSize | Out-String - Write-Host 'Add Azure Governance Visualizer htParameters succeeded' -ForegroundColor Green -} -function addIndexNumberToArray ( - [Parameter(Mandatory = $True)] - [array]$array -) { - for ($i = 0; $i -lt ($array).count; $i++) { - Add-Member -InputObject $array[$i] -Name '#' -Value ($i + 1) -MemberType NoteProperty - } - return $array -} -function addRowToTable() { - Param ( - [string]$level = 0, - [string]$mgName = '', - [string]$mgId = '', - [string]$mgParentId = '', - [string]$mgParentName = '', - [string]$mgASCSecureScore = '', - [string]$Subscription = '', - [string]$SubscriptionId = '', - [string]$SubscriptionQuotaId = '', - [string]$SubscriptionState = '', - [string]$SubscriptionASCSecureScore = '', - [string]$SubscriptionTags = '', - [int]$SubscriptionTagsCount = 0, - [string]$Policy = '', - [string]$PolicyAvailability = '', - [string]$PolicyDescription = '', - [string]$PolicyVariant = '', - [string]$PolicyType = '', - $PolicyIsALZ = '', - [string]$PolicyCategory = '', - [string]$PolicyDefinitionIdGuid = '', - [string]$PolicyDefinitionId = '', - [string]$PolicyDefintionScope = '', - [string]$PolicyDefintionScopeMgSub = '', - [string]$PolicyDefintionScopeId = '', - [int]$PolicyDefinitionsScopedLimit = 0, - [int]$PolicyDefinitionsScopedCount = 0, - [int]$PolicySetDefinitionsScopedLimit = 0, - [int]$PolicySetDefinitionsScopedCount = 0, - [string]$PolicyDefinitionEffectDefault = '', - [string]$PolicyDefinitionEffectFixed = '', - [string]$PolicyAssignmentScope = '', - [string]$PolicyAssignmentScopeMgSubRg = '', - [string]$PolicyAssignmentScopeName = '', - $PolicyAssignmentNotScopes = '', - [string]$PolicyAssignmentId = '', - [string]$PolicyAssignmentName = '', - [string]$PolicyAssignmentDisplayName = '', - [string]$PolicyAssignmentDescription = '', - [string]$PolicyAssignmentEnforcementMode = '', - $PolicyAssignmentNonComplianceMessages = '', - [string]$PolicyAssignmentIdentity = '', - [int]$PolicyAssignmentLimit = 0, - [int]$PolicyAssignmentCount = 0, - [int]$PolicyAssignmentAtScopeCount = 0, - $PolicyAssignmentParameters, - $PolicyAssignmentParametersFormated, - [int]$PolicySetAssignmentLimit = 0, - [int]$PolicySetAssignmentCount = 0, - [int]$PolicySetAssignmentAtScopeCount = 0, - [int]$PolicyAndPolicySetAssignmentAtScopeCount = 0, - [string]$PolicyAssignmentAssignedBy = '', - [string]$PolicyAssignmentCreatedBy = '', - [string]$PolicyAssignmentCreatedOn = '', - [string]$PolicyAssignmentUpdatedBy = '', - [string]$PolicyAssignmentUpdatedOn = '', - [string]$RoleDefinitionId = '', - [string]$RoleDefinitionName = '', - [string]$RoleAssignmentIdentityDisplayname = '', - [string]$RoleAssignmentIdentitySignInName = '', - [string]$RoleAssignmentIdentityObjectId = '', - [string]$RoleAssignmentIdentityObjectType = '', - [string]$RoleAssignmentId = '', - [string]$RoleAssignmentScope = '', - [string]$RoleAssignmentScopeName = '', - [string]$RoleAssignmentScopeRG = '', - [string]$RoleAssignmentScopeRes = '', - [string]$RoleAssignmentScopeType = '', - [string]$RoleAssignmentCreatedBy = '', - [string]$RoleAssignmentCreatedOn = '', - $RoleAssignmentCreatedOnUnformatted, - [string]$RoleAssignmentUpdatedBy = '', - [string]$RoleAssignmentUpdatedOn = '', - [string]$RoleIsCustom = '', - [string]$RoleAssignableScopes = '', - [int]$RoleAssignmentsLimit = 0, - [int]$RoleAssignmentsCount = 0, - [string]$RoleActions = '', - [string]$RoleNotActions = '', - [string]$RoleDataActions = '', - [string]$RoleNotDataActions = '', - $RoleCanDoRoleAssignments, - [int]$RoleSecurityCustomRoleOwner = 0, - [int]$RoleSecurityOwnerAssignmentSP = 0, - [string]$BlueprintName = '', - [string]$BlueprintId = '', - [string]$BlueprintDisplayName = '', - [string]$BlueprintDescription = '', - [string]$BlueprintScoped = '', - [string]$BlueprintAssignmentVersion = '', - [string]$BlueprintAssignmentId = '', - [string]$RoleAssignmentPIM = '', - [string]$RoleAssignmentPIMAssignmentType = '', - $RoleAssignmentPIMSlotStart = '', - $RoleAssignmentPIMSlotEnd = '' - ) - - $null = $script:newTable.Add([PSCustomObject]@{ - level = $level - mgName = $mgName - mgId = $mgId - mgParentId = $mgParentId - mgParentName = $mgParentName - mgASCSecureScore = $mgASCSecureScore - Subscription = $Subscription - SubscriptionId = $SubscriptionId - SubscriptionQuotaId = $SubscriptionQuotaId - SubscriptionState = $SubscriptionState - SubscriptionASCSecureScore = $SubscriptionASCSecureScore - SubscriptionTags = $SubscriptionTags - SubscriptionTagsCount = $SubscriptionTagsCount - Policy = $Policy - PolicyAvailability = $PolicyAvailability - PolicyDescription = $PolicyDescription - PolicyVariant = $PolicyVariant - PolicyType = $PolicyType - PolicyIsALZ = $PolicyIsALZ - PolicyCategory = $PolicyCategory - PolicyDefinitionIdGuid = $PolicyDefinitionIdGuid - PolicyDefinitionId = $PolicyDefinitionId - PolicyDefintionScope = $PolicyDefintionScope - PolicyDefintionScopeMgSub = $PolicyDefintionScopeMgSub - PolicyDefintionScopeId = $PolicyDefintionScopeId - PolicyDefinitionsScopedLimit = $PolicyDefinitionsScopedLimit - PolicyDefinitionsScopedCount = $PolicyDefinitionsScopedCount - PolicySetDefinitionsScopedLimit = $PolicySetDefinitionsScopedLimit - PolicySetDefinitionsScopedCount = $PolicySetDefinitionsScopedCount - PolicyDefinitionEffectDefault = $PolicyDefinitionEffectDefault - PolicyDefinitionEffectFixed = $PolicyDefinitionEffectFixed - PolicyAssignmentScope = $PolicyAssignmentScope - PolicyAssignmentScopeMgSubRg = $PolicyAssignmentScopeMgSubRg - PolicyAssignmentScopeName = $PolicyAssignmentScopeName - PolicyAssignmentNotScopes = $PolicyAssignmentNotScopes - PolicyAssignmentId = $PolicyAssignmentId - PolicyAssignmentName = $PolicyAssignmentName - PolicyAssignmentDisplayName = $PolicyAssignmentDisplayName - PolicyAssignmentDescription = $PolicyAssignmentDescription - PolicyAssignmentEnforcementMode = $PolicyAssignmentEnforcementMode - PolicyAssignmentNonComplianceMessages = $PolicyAssignmentNonComplianceMessages - PolicyAssignmentIdentity = $PolicyAssignmentIdentity - PolicyAssignmentLimit = $PolicyAssignmentLimit - PolicyAssignmentCount = $PolicyAssignmentCount - PolicyAssignmentAtScopeCount = $PolicyAssignmentAtScopeCount - PolicyAssignmentParameters = $PolicyAssignmentParameters - PolicyAssignmentParametersFormated = $PolicyAssignmentParametersFormated - PolicySetAssignmentLimit = $PolicySetAssignmentLimit - PolicySetAssignmentCount = $PolicySetAssignmentCount - PolicySetAssignmentAtScopeCount = $PolicySetAssignmentAtScopeCount - PolicyAndPolicySetAssignmentAtScopeCount = $PolicyAndPolicySetAssignmentAtScopeCount - PolicyAssignmentAssignedBy = $PolicyAssignmentAssignedBy - PolicyAssignmentCreatedBy = $PolicyAssignmentCreatedBy - PolicyAssignmentCreatedOn = $PolicyAssignmentCreatedOn - PolicyAssignmentUpdatedBy = $PolicyAssignmentUpdatedBy - PolicyAssignmentUpdatedOn = $PolicyAssignmentUpdatedOn - RoleDefinitionId = $RoleDefinitionId - RoleDefinitionName = $RoleDefinitionName - RoleAssignmentIdentityDisplayname = $RoleAssignmentIdentityDisplayname - RoleAssignmentIdentitySignInName = $RoleAssignmentIdentitySignInName - RoleAssignmentIdentityObjectId = $RoleAssignmentIdentityObjectId - RoleAssignmentIdentityObjectType = $RoleAssignmentIdentityObjectType - RoleAssignmentId = $RoleAssignmentId - RoleAssignmentScope = $RoleAssignmentScope - RoleAssignmentScopeName = $RoleAssignmentScopeName - RoleAssignmentScopeRG = $RoleAssignmentScopeRG - RoleAssignmentScopeRes = $RoleAssignmentScopeRes - RoleAssignmentScopeType = $RoleAssignmentScopeType - RoleIsCustom = $RoleIsCustom - RoleAssignableScopes = $RoleAssignableScopes - RoleAssignmentCreatedBy = $RoleAssignmentCreatedBy - RoleAssignmentCreatedOn = $RoleAssignmentCreatedOn - RoleAssignmentCreatedOnUnformatted = $RoleAssignmentCreatedOnUnformatted - RoleAssignmentUpdatedBy = $RoleAssignmentUpdatedBy - RoleAssignmentUpdatedOn = $RoleAssignmentUpdatedOn - RoleAssignmentsLimit = $RoleAssignmentsLimit - RoleAssignmentsCount = $RoleAssignmentsCount - RoleActions = $RoleActions - RoleNotActions = $RoleNotActions - RoleDataActions = $RoleDataActions - RoleNotDataActions = $RoleNotDataActions - RoleCanDoRoleAssignments = $RoleCanDoRoleAssignments - RoleSecurityCustomRoleOwner = $RoleSecurityCustomRoleOwner - RoleSecurityOwnerAssignmentSP = $RoleSecurityOwnerAssignmentSP - BlueprintName = $BlueprintName - BlueprintId = $BlueprintId - BlueprintDisplayName = $BlueprintDisplayName - BlueprintDescription = $BlueprintDescription - BlueprintScoped = $BlueprintScoped - BlueprintAssignmentVersion = $BlueprintAssignmentVersion - BlueprintAssignmentId = $BlueprintAssignmentId - RoleAssignmentPIM = $RoleAssignmentPIM - RoleAssignmentPIMAssignmentType = $RoleAssignmentPIMAssignmentType - RoleAssignmentPIMSlotStart = $RoleAssignmentPIMSlotStart - RoleAssignmentPIMSlotEnd = $RoleAssignmentPIMSlotEnd - }) -} -function apiCallTracking { - [CmdletBinding()]Param( - [string]$stage, - [string]$spacing - ) - #APITracking - $APICallTrackingCount = ($azAPICallConf['arrayAPICallTracking']).Count - $APICallTrackingRetriesCount = ($azAPICallConf['arrayAPICallTracking'].where({ $_.TryCounter -gt 1 } )).Count - $APICallTrackingGroupedByTargetEndpoint = $azAPICallConf['arrayAPICallTracking'] | Group-Object -Property TargetEndpoint - $APICallTrackingRestartDueToDuplicateNextlinkCounterCount = ($azAPICallConf['arrayAPICallTracking'].where({ $_.RestartDueToDuplicateNextlinkCounter -gt 0 } )).Count - Write-Host "$($spacing)$($stage) API call stats:" - $duarationStats = ($azAPICallConf['arrayAPICallTracking'].Duration | Measure-Object -Average -Maximum -Minimum) - Write-Host "$($spacing) API calls total count: $APICallTrackingCount ($APICallTrackingRetriesCount retries; $APICallTrackingRestartDueToDuplicateNextlinkCounterCount nextLinkReset) | average: $($duarationStats.Average) sec, maximum: $($duarationStats.Maximum) sec, minimum: $($duarationStats.Minimum) sec" - foreach ($targetEndpoint in $APICallTrackingGroupedByTargetEndpoint | Sort-Object -Property Name) { - $APICallTrackingRetriesCount = ($targetEndpoint.Group.where({ $_.TryCounter -gt 1 } )).Count - $APICallTrackingRestartDueToDuplicateNextlinkCounterCount = ($targetEndpoint.Group.where({ $_.RestartDueToDuplicateNextlinkCounter -gt 0 } )).Count - $duarationStats = ($targetEndpoint.Group.Duration | Measure-Object -Average -Maximum -Minimum) - Write-Host "$($spacing) API calls endpoint '$($targetEndpoint.Name) ($($azAPICallConf['azAPIEndpointUrls'].($targetEndpoint.Name)))' count: $($targetEndpoint.Count) ($APICallTrackingRetriesCount retries; $APICallTrackingRestartDueToDuplicateNextlinkCounterCount nextLinkReset) | average: $($duarationStats.Average) sec, maximum: $($duarationStats.Maximum) sec, minimum: $($duarationStats.Minimum) sec" - } -} -function buildJSON { - #$fileTimestamp = Get-Date -Format "yyyyMM-dd HHmmss" - $startJSON = Get-Date - $startBuildHt = Get-Date - - Write-Host 'Create Hierarchy JSON' - Write-Host ' Create ht for JSON' - - $htJSON = [ordered]@{} - $htJSON.ManagementGroups = [ordered]@{} - - $MgIds = ($optimizedTableForPathQuery) | Select-Object -Property level, MgId, MgName, mgParentId, mgParentName | Sort-Object -Property level, MgId -Unique - $grpScopePolicyDefinitionsCustom = (($htCacheDefinitionsPolicy).values).where( { $_.Type -eq 'Custom' }) | Group-Object ScopeMgSub - $grpMgScopePolicyDefinitionsCustom = ($grpScopePolicyDefinitionsCustom.where( { $_.Name -eq 'Mg' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId) - $grpSubScopePolicyDefinitionsCustom = ($grpScopePolicyDefinitionsCustom.where( { $_.Name -eq 'Sub' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId) - - $grpScopePolicySetDefinitionsCustom = (($htCacheDefinitionsPolicySet).values).where( { $_.Type -eq 'Custom' }) | Group-Object ScopeMgSub - $grpMgScopePolicySetDefinitionsCustom = $grpScopePolicySetDefinitionsCustom.where( { $_.Name -eq 'Mg' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId - $grpSubScopePolicySetDefinitionsCustom = $grpScopePolicySetDefinitionsCustom.where( { $_.Name -eq 'Sub' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId - - $grpScopePolicyAssignments = ($htCacheAssignmentsPolicy).values | Group-Object -Property AssignmentScopeMgSubRg - $grpMgScopePolicyAssignments = $grpScopePolicyAssignments.where( { $_.Name -eq 'Mg' }).Group | Sort-Object @{Expression = { $_.Assignment.Id } } | Group-Object -Property AssignmentScopeId - $grpSubScopePolicyAssignments = $grpScopePolicyAssignments.where( { $_.Name -eq 'Sub' }).Group | Sort-Object @{Expression = { $_.Assignment.Id } } | Group-Object -Property AssignmentScopeId - - if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { - if (-not $JsonExportExcludeResourceGroups) { - $grpRGScopePolicyAssignments = $grpScopePolicyAssignments.where( { $_.Name -eq 'RG' }).Group | Sort-Object @{Expression = { $_.Assignment.Id } } | Group-Object -Property AssignmentScopeId - $htSubRGPolicyAssignments = @{} - foreach ($rgpa in $grpRGScopePolicyAssignments) { - $subId = ($rgpa.Name).split('/')[0] - if (-not $htSubRGPolicyAssignments.($subId)) { - $htSubRGPolicyAssignments.($subId) = @{} - } - if (-not $htSubRGPolicyAssignments.($subId).PolicyAssignments) { - $htSubRGPolicyAssignments.($subId).PolicyAssignments = @() - } - $htSubRGPolicyAssignments.($subId).PolicyAssignments += $rgpa.group - } - } - } - - $grpScopeRoleAssignments = ($htCacheAssignmentsRole).values | Group-Object -Property AssignmentScopeTenMgSubRgRes - $grpTenantScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Tenant' }).Group | Group-Object -Property AssignmentScopeId - $grpMgScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Mg' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId - $grpSubScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Sub' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId - - if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { - if (-not $JsonExportExcludeResourceGroups) { - $grpRGScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'RG' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId - $htSubRGRoleAssignments = @{} - foreach ($rgra in $grpRGScopeRoleAssignments) { - $subId = ($rgra.Name).split('/')[0] - if (-not $htSubRGRoleAssignments.($subId)) { - $htSubRGRoleAssignments.($subId) = @{} - } - if (-not $htSubRGRoleAssignments.($subId).RoleAssignments) { - $htSubRGRoleAssignments.($subId).RoleAssignments = @() - } - $htSubRGRoleAssignments.($subId).RoleAssignments += $rgra.group - } - - #res - if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { - if (-not $JsonExportExcludeResources) { - $grpResScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Res' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId - $htSubResRoleAssignments = @{} - foreach ($resra in $grpResScopeRoleAssignments.Group) { - $raSplit = ($resra.Assignment.RoleAssignmentId).split('/') - $splitSubId = $raSplit[2] - $splitRg = $raSplit[4] - if (-not $htSubResRoleAssignments.($splitSubId)) { - $htSubResRoleAssignments.($splitSubId) = @{} - } - if (-not $htSubResRoleAssignments.($splitSubId).($splitRg)) { - $htSubResRoleAssignments.($splitSubId).($splitRg) = @{} - - } - - $resourceName = $resra.AssignmentScopeId.split('/')[2] - if (-not $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)")) { - $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)") = @{} - - } - if (-not $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)").RoleAssignments) { - $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)").RoleAssignments = [ordered]@{} - - } - ($htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)").RoleAssignments.($resra.Assignment.RoleAssignmentId)) = $resra.Assignment - } - } - } - } - - } - - $bluePrintsAssignmentsAtScope = ($htCacheAssignmentsBlueprint).keys | Sort-Object - $bluePrintDefinitions = ($htCacheDefinitionsBlueprint).Keys | Sort-Object - $subscriptions = ($optimizedTableForPathQuery.where( { -not [string]::IsNullOrEmpty($_.subscriptionId) })) | Select-Object mgId, Subscription* | Sort-Object -Property subscriptionId -Unique - foreach ($mg in $MgIds) { - - $htJSON.ManagementGroups.($mg.MgId) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).MgId = $mg.MgId - $htJSON.ManagementGroups.($mg.MgId).MgName = $mg.MgName - $htJSON.ManagementGroups.($mg.MgId).mgParentId = $mg.mgParentId - $htJSON.ManagementGroups.($mg.MgId).mgParentName = $mg.mgParentName - $htJSON.ManagementGroups.($mg.MgId).level = $mg.level - $htJSON.ManagementGroups.($mg.MgId).PolicyDefinitionsCustom = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).PolicySetDefinitionsCustom = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).BlueprintDefinitions = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).PolicyAssignments = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).RoleAssignments = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions = [ordered]@{} - - foreach ($PolDef in (($grpMgScopePolicyDefinitionsCustom).where( { $_.Name -eq $mg.MgId })).group) { - $htJSON.ManagementGroups.($mg.MgId).PolicyDefinitionsCustom.($PolDef.Id) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).PolicyDefinitionsCustom.($PolDef.Id) = $PolDef.Json - } - - foreach ($PolSetDef in (($grpMgScopePolicySetDefinitionsCustom).where( { $_.Name -eq $mg.MgId })).group) { - $htJSON.ManagementGroups.($mg.MgId).PolicySetDefinitionsCustom.($PolSetDef.Id) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).PolicySetDefinitionsCustom.($PolSetDef.Id) = $PolSetDef.Json - } - - foreach ($PolAssignment in ($grpMgScopePolicyAssignments).where( { $_.Name -eq $mg.MgId }).group) { - $htJSON.ManagementGroups.($mg.MgId).PolicyAssignments.($PolAssignment.Assignment.id) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).PolicyAssignments.($PolAssignment.Assignment.id) = $PolAssignment.Assignment - } - - foreach ($RoleAssignment in ($grpMgScopeRoleAssignments).where( { $_.Name -eq $mg.MgId }).group) { - $htJSON.ManagementGroups.($mg.MgId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = $RoleAssignment.Assignment - } - - foreach ($BlueprintDefinition in ($bluePrintDefinitions).where( { $_ -like "/providers/Microsoft.Management/managementGroups/$($mg.MgId)/*" })) { - $htJSON.ManagementGroups.($mg.MgId).BlueprintDefinitions.($BlueprintDefinition) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).BlueprintDefinitions.($BlueprintDefinition) = $BlueprintDefinition - } - - if (($htDiagnosticSettingsMgSub).mg.($mg.MgId)) { - foreach ($entry in ($htDiagnosticSettingsMgSub).mg.($mg.MgId).keys | Sort-Object) { - $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry) = [ordered]@{} - foreach ($diagset in ($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.keys | Sort-Object) { - $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).Name = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticSettingName) - $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).Type = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticTargetType) - $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).TargetId = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticTargetId) - $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).Settings = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticCategories) - } - } - } - - foreach ($subscription in $subscriptions) { - if ($subscription.MgId -eq $mg.MgId) { - - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionName = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionQuotaId = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionState = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionTags = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionName = $subscription.Subscription - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionQuotaId = $subscription.SubscriptionQuotaId - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionState = $subscription.SubscriptionState - if ($htSubscriptionTags.($subscription.SubscriptionId)) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionTags = $htSubscriptionTags.($subscription.SubscriptionId).getEnumerator() | Sort-Object Key -CaseSensitive - } - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyDefinitionsCustom = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicySetDefinitionsCustom = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintDefinitions = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyAssignments = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).RoleAssignments = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintAssignments = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings = [ordered]@{} - - foreach ($PolDef in (($grpSubScopePolicyDefinitionsCustom).where( { $_.Name -eq $subscription.subscriptionId })).group) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyDefinitionsCustom.($PolDef.Id) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyDefinitionsCustom.($PolDef.Id) = $PolDef.Json - } - - foreach ($PolSetDef in (($grpSubScopePolicySetDefinitionsCustom).where( { $_.Name -eq $subscription.subscriptionId })).group) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicySetDefinitionsCustom.($PolSetDef.Id) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicySetDefinitionsCustom.($PolSetDef.Id) = $PolSetDef.Json - } - - foreach ($PolAssignment in ($grpSubScopePolicyAssignments).where( { $_.Name -eq $subscription.subscriptionId }).group) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyAssignments.($PolAssignment.Assignment.id) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyAssignments.($PolAssignment.Assignment.id) = $PolAssignment.Assignment - } - - foreach ($RoleAssignment in ($grpSubScopeRoleAssignments).where( { $_.Name -eq $subscription.subscriptionId }).group) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = $RoleAssignment.Assignment - } - - foreach ($BlueprintDefinition in ($bluePrintDefinitions).where( { $_ -like "/subscriptions/$($subscription.subscriptionId)/*" })) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintDefinitions.($BlueprintDefinition) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintDefinitions.($BlueprintDefinition) = $BlueprintDefinition - } - - foreach ($BlueprintsAssignment in ($blueprintsAssignmentsAtScope).where( { $_ -like "/subscriptions/$($subscription.subscriptionId)/*" })) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintAssignments.($BlueprintsAssignment) = [ordered]@{} - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintAssignments.($BlueprintsAssignment) = $BlueprintsAssignment - } - - if (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId)) { - foreach ($entry in ($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).keys | Sort-Object) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry) = [ordered]@{} - foreach ($diagset in ($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.keys | Sort-Object) { - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).Name = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticSettingName) - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).Type = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticTargetType) - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).TargetId = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticTargetId) - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).Settings = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticCategories) - } - } - } - - - if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { - if (-not $JsonExportExcludeResourceGroups) { - $htTemp = @{} - if (-not $htTemp.ResourceGroups) { - $htTemp.ResourceGroups = @{} - } - - if ($htSubRGPolicyAssignments.($subscription.subscriptionId)) { - foreach ($rgpa in $htSubRGPolicyAssignments.($subscription.subscriptionId).PolicyAssignments) { - $rgName = ($rgpa.AssignmentScopeId).split('/')[1] - if (-not $htTemp.ResourceGroups.($rgName)) { - $htTemp.ResourceGroups.($rgName) = [ordered]@{} - } - if (-not $htTemp.ResourceGroups.($rgName).PolicyAssignments) { - $htTemp.ResourceGroups.($rgName).PolicyAssignments = [ordered]@{} - } - $htTemp.ResourceGroups.($rgName).PolicyAssignments.($rgpa.Assignment.id) = $rgpa.Assignment - } - } - } - } - - if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { - if (-not $JsonExportExcludeResourceGroups) { - if (-not $htTemp) { - $htTemp = @{} - } - if (-not $htTemp.ResourceGroups) { - $htTemp.ResourceGroups = @{} - } - if ($htSubRGRoleAssignments.($subscription.subscriptionId)) { - foreach ($rgra in $htSubRGRoleAssignments.($subscription.subscriptionId).RoleAssignments) { - $rgName = ($rgra.AssignmentScopeId).split('/')[1] - if (-not $htTemp.ResourceGroups.($rgName)) { - $htTemp.ResourceGroups.($rgName) = [ordered]@{} - } - if (-not $htTemp.ResourceGroups.($rgName).RoleAssignments) { - $htTemp.ResourceGroups.($rgName).RoleAssignments = [ordered]@{} - } - $htTemp.ResourceGroups.($rgName).RoleAssignments.($rgra.Assignment.RoleAssignmentId) = $rgra.Assignment - } - } - # - if (-not $JsonExportExcludeResources) { - if (-not $htTemp.ResourceGroups) { - $htTemp.ResourceGroups = @{} - } - if ($htSubResRoleAssignments.($subscription.subscriptionId)) { - foreach ($rg in $htSubResRoleAssignments.($subscription.subscriptionId).keys) { - foreach ($res in $htSubResRoleAssignments.($subscription.subscriptionId).($rg).Keys | Sort-Object) { - $rgName = ($resra.AssignmentScopeId).split('/')[1] - if (-not $htTemp.ResourceGroups.($rg)) { - $htTemp.ResourceGroups.($rg) = [ordered]@{} - } - if (-not $htTemp.ResourceGroups.($rg).Resources) { - $htTemp.ResourceGroups.($rg).Resources = [ordered]@{} - } - if (-not $htTemp.ResourceGroups.($rg).Resources.($res)) { - $htTemp.ResourceGroups.($rg).Resources.($res) = [ordered]@{} - } - if (-not $htTemp.ResourceGroups.($rg).Resources.($res).RoleAssignments) { - $htTemp.ResourceGroups.($rg).Resources.($res).RoleAssignments = [ordered]@{} - } - $htTemp.ResourceGroups.($rg).Resources.($res).RoleAssignments = $htSubResRoleAssignments.($subscription.subscriptionId).($rg).($res).RoleAssignments - } - } - } - } - } - } - - if ($htTemp) { - $sortedHt = [ordered]@{} - foreach ($key in ($htTemp.ResourceGroups.keys | Sort-Object)) { - $sortedHt.($key) = $htTemp.ResourceGroups.($key) - } - $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).ResourceGroups = $sortedHt - $htTemp = $null - $sortedHt = $null - } - } - } - } - - if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) { - if ($ManagementGroupsOnly) { - $JSONPath = "JSON_ManagementGroupsOnly_$($ManagementGroupId)" - } - else { - $JSONPath = "JSON_$($ManagementGroupId)" - } - - if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)") { - Write-Host ' Cleaning old state (Pipeline only)' - Remove-Item -Recurse -Force "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)" - } - } - else { - if ($ManagementGroupsOnly) { - $JSONPath = "JSON_ManagementGroupsOnly_$($ManagementGroupId)_$($fileTimestamp)" - } - else { - $JSONPath = "JSON_$($ManagementGroupId)_$($fileTimestamp)" - } - Write-Host " Creating new state ($($JSONPath)) (local only))" - } - - $null = New-Item -Name $JSONPath -ItemType directory -Path $outputPath - - if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) { - "The directory '$($JSONPath)' will be rebuilt during the AzDO Pipeline run. __Do not save any files in this directory, files and folders will be deleted!__" | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)ReadMe_important.md" -Encoding utf8 - } - - $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Definitions" -ItemType directory -Path $outputPath - - - - - $htJSON.RoleDefinitions = [ordered]@{} - $pathRoleDefinitions = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)RoleDefinitions" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitions)")) { - $null = New-Item -Name $pathRoleDefinitions -ItemType directory -Path $outputPath - $pathRoleDefinitionCustom = "$($pathRoleDefinitions)$($DirectorySeparatorChar)Custom" - $pathRoleDefinitionBuiltIn = "$($pathRoleDefinitions)$($DirectorySeparatorChar)BuiltIn" - $null = New-Item -Name "$($pathRoleDefinitionCustom)" -ItemType directory -Path $outputPath - $null = New-Item -Name "$($pathRoleDefinitionBuiltIn)" -ItemType directory -Path $outputPath - } - - if (($htCacheDefinitionsRole).Keys.Count -gt 0) { - foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { ($htCacheDefinitionsRole).($_).IsCustom }) | Sort-Object) { - $htJSON.RoleDefinitions.($roleDefinition) = ($htCacheDefinitionsRole).($roleDefinition).Json.properties - $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json.properties | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionCustom)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 - } - foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { -not ($htCacheDefinitionsRole).($_).IsCustom })) { - $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name ) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 - } - } - - $pathPolicyDefinitions = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicyDefinitions" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitions)")) { - $null = New-Item -Name $pathPolicyDefinitions -ItemType directory -Path $outputPath - $pathPolicyDefinitionBuiltIn = "$($pathPolicyDefinitions)$($DirectorySeparatorChar)BuiltIn" - $null = New-Item -Name "$($pathPolicyDefinitionBuiltIn)" -ItemType directory -Path $outputPath - } - if (($htCacheDefinitionsPolicy).Keys.Count -gt 0) { - foreach ($policyDefinition in ($htCacheDefinitionsPolicy).Keys.where( { ($htCacheDefinitionsPolicy).($_).Type -eq 'BuiltIn' })) { - $jsonConverted = ($htCacheDefinitionsPolicy).($policyDefinition).Json.properties | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicy).($policyDefinition).displayName) ($(($htCacheDefinitionsPolicy).($policyDefinition).Json.name)).json" -Encoding utf8 - } - } - - $pathPolicySetDefinitions = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicySetDefinitions" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitions)")) { - $null = New-Item -Name $pathPolicySetDefinitions -ItemType directory -Path $outputPath - $pathPolicySetDefinitionBuiltIn = "$($pathPolicySetDefinitions)$($DirectorySeparatorChar)BuiltIn" - $null = New-Item -Name "$($pathPolicySetDefinitionBuiltIn)" -ItemType directory -Path $outputPath - } - if (($htCacheDefinitionsPolicySet).Keys.Count -gt 0) { - foreach ($policySetDefinition in ($htCacheDefinitionsPolicySet).Keys.where( { ($htCacheDefinitionsPolicySet).($_).Type -eq 'BuiltIn' })) { - $jsonConverted = ($htCacheDefinitionsPolicySet).($policySetDefinition).Json.properties | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicySet).($policySetDefinition).displayName) ($(($htCacheDefinitionsPolicySet).($policySetDefinition).Json.name)).json" -Encoding utf8 - } - } - - $endBuildHt = Get-Date - Write-Host " ht for JSON creation duration: $((New-TimeSpan -Start $startBuildHt -End $endBuildHt).TotalSeconds) seconds" - - $startBuildJSON = Get-Date - Write-Host ' Build JSON' - - - $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Tenant" -ItemType directory -Path $outputPath - - $htTree = [ordered]@{} - $htTree.'Tenant' = [ordered] @{} - $htTree.Tenant.TenantId = $azAPICallConf['checkContext'].Tenant.Id - $htTree.Tenant.RoleAssignments = [ordered]@{} - foreach ($RoleAssignment in ($grpTenantScopeRoleAssignments).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } }) { - - $htTree.Tenant.RoleAssignments.$($RoleAssignment.Assignment.RoleAssignmentId) = [ordered]@{} - $htTree.Tenant.RoleAssignments.$($RoleAssignment.Assignment.RoleAssignmentId) = $RoleAssignment.Assignment - - if ($RoleAssignment.Assignment.PIM -eq 'true') { - $pim = 'PIM_' - } - else { - $pim = '' - } - $jsonConverted = ($RoleAssignment.Assignment | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Tenant$($DirectorySeparatorChar)ra_$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Tenant" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - } - - $htTree.'Tenant'.'ManagementGroups' = [ordered] @{} - $json = $htTree.'Tenant' - - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Assignments")) { - $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Assignments" -ItemType directory -Path $outputPath - } - - buildTree -mgId $ManagementGroupId -json $json -prnt "$($JSONPath)$($DirectorySeparatorChar)Tenant" - - $htTree.'Tenant'.'CustomRoleDefinitions' = $htJSON.RoleDefinitions - - Write-Host " Exporting Tenant JSON '$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)$($fileName).json'" - $htTree | ConvertTo-Json -Depth 99 | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)$($fileName).json" -Encoding utf8 -Force - - $endBuildJSON = Get-Date - Write-Host " Building JSON duration: $((New-TimeSpan -Start $startBuildJSON -End $endBuildJSON).TotalSeconds) seconds" - - $endJSON = Get-Date - Write-Host "Creating Hierarchy JSON duration: $((New-TimeSpan -Start $startJSON -End $endJSON).TotalSeconds) seconds" -} -function buildMD { - Write-Host 'Building Markdown' - $startBuildMD = Get-Date - $script:arrayMgs = [System.Collections.ArrayList]@() - $script:arraySubs = [System.Collections.ArrayList]@() - $script:arraySubsOos = [System.Collections.ArrayList]@() - $markdown = $null - $script:markdownhierarchyMgs = $null - $script:markdownhierarchySubs = $null - $script:markdownTable = $null - - if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { - if ($azAPICallConf['htParameters'].onAzureDevOps -eq $true) { - $markdown += @" -# Azure Governance Visualizer - Management Group Hierarchy - -## HierarchyMap (Mermaid) - -::: mermaid - graph $($MermaidDirection.ToUpper());`n -"@ - } - if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { - $marks = '```' - $markdown += @" -# Azure Governance Visualizer - Management Group Hierarchy - -## HierarchyMap (Mermaid) - -$($marks)mermaid - graph $($MermaidDirection.ToUpper());`n -"@ - } - - } - else { - $markdown += @" -# Azure Governance Visualizer - Management Group Hierarchy - -$executionDateTimeInternationalReadable ($currentTimeZone) - -## HierarchyMap (Mermaid) - -::: mermaid - graph $($MermaidDirection.ToUpper());`n -"@ - } - - processDiagramMermaid - - $markdown += @" -$markdownhierarchyMgs -$markdownhierarchySubs - classDef mgr fill:#D9F0FF,stroke:#56595E,color:#000000,stroke-width:1px; - classDef subs fill:#EEEEEE,stroke:#56595E,color:#000000,stroke-width:1px; -"@ - - if (($arraySubsOos).count -gt 0) { - $markdown += @' - classDef subsoos fill:#FFCBC7,stroke:#56595E,color:#000000,stroke-width:1px; -'@ - } - - $markdown += @" - classDef mgrprnts fill:#FFFFFF,stroke:#56595E,color:#000000,stroke-width:1px; - class $(($arrayMgs | Sort-Object -Unique) -join ',') mgr; - class $(($arraySubs | Sort-Object -Unique) -join ',') subs; -"@ - - if (($arraySubsOos).count -gt 0) { - $markdown += @" - class $(($arraySubsOos | Sort-Object -Unique) -join ',') subsoos; -"@ - } - - if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { - if ($azAPICallConf['htParameters'].onAzureDevOps -eq $true) { - $markdown += @" -class $mermaidprnts mgrprnts; -::: - -"@ - } - if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { -` - $marks = '```' - $markdown += @" -class $mermaidprnts mgrprnts; -$marks - -"@ - } - } - else { - $markdown += @" -class $mermaidprnts mgrprnts; -::: - -"@ - } - - $markdown += @" -## Summary -`n -"@ - if (-not $HierarchyMapOnly) { - $markdown += @" -Total Management Groups: $totalMgCount (depth $mgDepth)\`n -"@ - - if (($arraySubsOos).count -gt 0) { - $markdown += @" -Total Subscriptions: $totalSubIncludedAndExcludedCount ($totalSubOutOfScopeCount out-of-scope)\`n -"@ - } - else { - $markdown += @" -Total Subscriptions: $totalSubIncludedAndExcludedCount\`n -"@ - } - - $markdown += @" -Total Custom Policy definitions: $tenantCustomPoliciesCount\ -Total Custom PolicySet definitions: $tenantCustompolicySetsCount\ -Total Policy assignments: $($totalPolicyAssignmentsCount)\ -Total Policy assignments ManagementGroups $($totalPolicyAssignmentsCountMg)\ -Total Policy assignments Subscriptions $($totalPolicyAssignmentsCountSub)\ -Total Policy assignments ResourceGroups: $($totalPolicyAssignmentsCountRg)\ -Total Custom Role definitions: $totalRoleDefinitionsCustomCount\ -Total Role assignments: $totalRoleAssignmentsCount\ -Total Role assignments (Tenant): $totalRoleAssignmentsCountTen\ -Total Role assignments (ManagementGroups): $totalRoleAssignmentsCountMG\ -Total Role assignments (Subscriptions): $totalRoleAssignmentsCountSub\ -Total Role assignments (ResourceGroups and Resources): $totalRoleAssignmentsResourceGroupsAndResourcesCount\ -Total Blueprint definitions: $totalBlueprintDefinitionsCount\ -Total Blueprint assignments: $totalBlueprintAssignmentsCount\ -Total Resources: $totalResourceCount\ -Total Resource Types: $totalResourceTypesCount -"@ - - } - if ($HierarchyMapOnly) { - $mgsDetails = ($optimizedTableForPathQueryMg | Select-Object Level, MgId -Unique) - $mgDepth = ($mgsDetails.Level | Measure-Object -Maximum).Maximum - $totalMgCount = ($mgsDetails).count - $totalSubCount = ($optimizedTableForPathQuerySub).count - - $markdown += @" -Total Management Groups: $totalMgCount (depth $mgDepth)\ -Total Subscriptions: $totalSubCount -"@ - - } - - $markdown += @" -`n -## Hierarchy Table - -| **MgLevel** | **MgName** | **MgId** | **MgParentName** | **MgParentId** | **SubName** | **SubId** | -|-------------|-------------|-------------|-------------|-------------|-------------|-------------| -$markdownTable -"@ - - $markdown | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).md" -Encoding utf8 -Force - $endBuildMD = Get-Date - Write-Host "Building Markdown total duration: $((New-TimeSpan -Start $startBuildMD -End $endBuildMD).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildMD -End $endBuildMD).TotalSeconds) seconds)" -} -function buildPolicyAllJSON { - Write-Host 'Creating PolicyAll JSON' - $startPolicyAllJSON = Get-Date - $htPolicyAndPolicySet = [ordered]@{} - $htPolicyAndPolicySet.Policy = [ordered]@{} - $htPolicyAndPolicySet.PolicySet = [ordered]@{} - $htPolicyAndPolicySet.PolicyAssignment = [ordered]@{} - foreach ($policy in ($tenantPoliciesDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicyDefinitionId)) { - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()) = [ordered]@{ - PolicyType = $policy.Type - ScopeMGLevel = $policy.ScopeMGLevel - Scope = $policy.Scope - ScopeId = $policy.scopeId - PolicyDisplayName = $policy.PolicyDisplayName - PolicyDefinitionName = $policy.PolicyDefinitionName - PolicyDefinitionId = $policy.PolicyDefinitionId - PolicyEffect = $policy.PolicyEffect - PolicyCategory = $policy.PolicyCategory - UniqueAssignmentsCount = $policy.UniqueAssignmentsCount - UniqueAssignments = $policy.UniqueAssignments - UsedInPolicySetsCount = $policy.UsedInPolicySetsCount - UsedInPolicySets = $policy.UsedInPolicySet4JSON - CreatedOn = $policy.CreatedOn - CreatedBy = $policy.CreatedByJson - UpdatedOn = $policy.UpdatedOn - UpdatedBy = $policy.UpdatedByJson - JSON = $policy.Json - } - } - foreach ($policySet in ($tenantPolicySetsDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicySetDefinitionId)) { - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()) = [ordered]@{ - PolicySetType = $policySet.Type - ScopeMGLevel = $policySet.ScopeMGLevel - Scope = $policySet.Scope - ScopeId = $policySet.scopeId - PolicySetDisplayName = $policySet.PolicySetDisplayName - PolicySetDefinitionName = $policySet.PolicySetDefinitionName - PolicySetDefinitionId = $policySet.PolicySetDefinitionId - PolicySetCategory = $policySet.PolicySetCategory - UniqueAssignmentsCount = $policySet.UniqueAssignmentsCount - UniqueAssignments = $policySet.UniqueAssignments - PoliciesUsedCount = $policySet.PoliciesUsedCount - PoliciesUsed = $policySet.PoliciesUsed4JSON - CreatedOn = $policySet.CreatedOn - CreatedBy = $policySet.CreatedByJson - UpdatedOn = $policySet.UpdatedOn - UpdatedBy = $policySet.UpdatedByJson - JSON = $policySet.Json - } - } - foreach ($key in $htCacheAssignmentsPolicy.keys | Sort-Object) { - $htPolicyAndPolicySet.PolicyAssignment.($key.ToLower()) = $htCacheAssignmentsPolicy.($key).Assignment - } - Write-Host " Exporting PolicyAll JSON '$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json'" - $htPolicyAndPolicySet | ConvertTo-Json -Depth 99 | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json" -Encoding utf8 -Force - - $endPolicyAllJSON = Get-Date - Write-Host "Creating PolicyAll JSON duration: $((New-TimeSpan -Start $startPolicyAllJSON -End $endPolicyAllJSON).TotalSeconds) seconds" -} -function buildTree($mgId, $prnt) { - $getMg = $htEntities.values.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.id -eq $mgId }) - $childrenManagementGroups = $htEntities.values.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.parentId -eq "/providers/Microsoft.Management/managementGroups/$($getMg.Id)" }) - $mgNameValid = removeInvalidFileNameChars $getMg.Id - $mgDisplayNameValid = removeInvalidFileNameChars $getMg.displayName - $prntx = "$($prnt)$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)")) { - $null = New-Item -Name $prntx -ItemType directory -Path $outputPath - } - - if (-not $json.'ManagementGroups') { - $json.'ManagementGroups' = [ordered]@{} - } - $json = $json.'ManagementGroups'.($getMg.Id) = [ordered]@{} - foreach ($mgCap in $htJSON.ManagementGroups.($getMg.Id).keys) { - $json.$mgCap = $htJSON.ManagementGroups.($getMg.Id).$mgCap - if ($mgCap -eq 'PolicyDefinitionsCustom') { - $mgCapShort = 'pd' - foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pdc) - if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { - $displayName = 'noDisplayNameGiven' - } - else { - $displayName = removeInvalidFileNameChars $hlp.properties.displayName - } - $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - } - } - if ($mgCap -eq 'PolicySetDefinitionsCustom') { - $mgCapShort = 'psd' - foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($psdc) - if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { - $displayName = 'noDisplayNameGiven' - } - else { - $displayName = removeInvalidFileNameChars $hlp.properties.displayName - } - $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - } - } - if ($mgCap -eq 'PolicyAssignments') { - $mgCapShort = 'pa' - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pa) - if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { - $displayName = 'noDisplayNameGiven' - } - else { - $displayName = removeInvalidFileNameChars $hlp.properties.displayName - } - $jsonConverted = $hlp | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($mgCap)$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - } - } - #marker - if ($mgCap -eq 'RoleAssignments') { - $mgCapShort = 'ra' - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($ra) - if ($hlp.PIM -eq 'true') { - $pim = 'PIM_' - } - else { - $pim = '' - } - $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($mgCap)$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - } - } - - if ($mgCap -eq 'Subscriptions') { - foreach ($sub in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $subNameValid = removeInvalidFileNameChars $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).SubscriptionName - $subFolderName = "$($prntx)$($DirectorySeparatorChar)$($subNameValid) ($($sub))" - $null = New-Item -Name $subFolderName -ItemType directory -Path $outputPath - foreach ($subCap in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).Keys) { - if ($subCap -eq 'PolicyDefinitionsCustom') { - $subCapShort = 'pd' - foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pdc) - if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { - $displayName = 'noDisplayNameGiven' - } - else { - $displayName = removeInvalidFileNameChars $hlp.properties.displayName - } - $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - } - } - if ($subCap -eq 'PolicySetDefinitionsCustom') { - $subCapShort = 'psd' - foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($psdc) - if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { - $displayName = 'noDisplayNameGiven' - } - else { - $displayName = removeInvalidFileNameChars $hlp.properties.displayName - } - $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - } - } - if ($subCap -eq 'PolicyAssignments') { - $subCapShort = 'pa' - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pa) - if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { - $displayName = 'noDisplayNameGiven' - } - else { - $displayName = removeInvalidFileNameChars $hlp.properties.displayName - } - $jsonConverted = $hlp | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($subCap)$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 - } - } - #marker - if ($subCap -eq 'RoleAssignments') { - $subCapShort = 'ra' - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($ra) - if ($hlp.PIM -eq 'true') { - $pim = 'PIM_' - } - else { - $pim = '' - } - $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($pim)$($hlp.ObjectType)_$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($subCap)$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - } - } - - #RG Pol - if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { - if (-not $JsonExportExcludeResourceGroups) { - if ($subCap -eq 'ResourceGroups') { - foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { - $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" - } - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.($pa) - if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { - $displayName = 'noDisplayNameGiven' - } - else { - $displayName = removeInvalidFileNameChars $hlp.properties.displayName - } - $jsonConverted = $hlp | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)pa_$($displayName) ($($hlp.name)).json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)PolicyAssignments$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))$($DirectorySeparatorChar)$($rg)" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($($hlp.name)).json" -Encoding utf8 - } - } - } - } - } - - #RG RoleAss - #marker - if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { - if (-not $JsonExportExcludeResourceGroups) { - if ($subCap -eq 'ResourceGroups') { - foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { - $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" - } - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.($ra) - if ($hlp.PIM -eq 'true') { - $pim = 'PIM_' - } - else { - $pim = '' - } - $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)ra_$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))$($DirectorySeparatorChar)$($rg)" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - } - #res - if (-not $JsonExportExcludeResources) { - - foreach ($res in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.keys) { - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)")) { - $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)" -ItemType directory -Path "$($outputPath)" - } - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.($ra) - if ($hlp.PIM -eq 'true') { - $pim = 'PIM_' - } - else { - $pim = '' - } - $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)$($DirectorySeparatorChar)ra_$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)" - if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { - $null = New-Item -Name $path -ItemType directory -Path $outputPath - } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 - } - } - } - } - } - } - } - } - } - } - } - - if ($childrenManagementGroups.Count -eq 0) { - $json.'ManagementGroups' = @{} - } - else { - foreach ($childMg in $childrenManagementGroups | Sort-Object -Property Id) { - buildTree -mgId $childMg.Id -json $json -prnt $prntx - } - } -} -function cacheBuiltIn { - $startDefinitionsCaching = Get-Date - Write-Host 'Caching built-in Policy and RBAC Role definitions' - - $arrayBuiltInCaching = @('PolicyDefinitions', 'PolicyDefinitionsStatic', 'PolicySetDefinitions', 'RoleDefinitions') - - $arrayBuiltInCaching | ForEach-Object -Parallel { - - $builtInCapability = $_ - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy - $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet - $htCacheDefinitionsRole = $using:htCacheDefinitionsRole - $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy - $ValidPolicyEffects = $using:ValidPolicyEffects - $htHashesBuiltInPolicy = $using:htHashesBuiltInPolicy - #vars - $ARMLocation = $using:ARMLocation - $ignoreARMLocation = $using:ignoreARMLocation - #functions - $function:detectPolicyEffect = $using:funcDetectPolicyEffect - $function:getPolicyHash = $using:funcGetPolicyHash - - if ($builtInCapability -eq 'PolicyDefinitions') { - $currentTask = 'Caching built-in Policy definitions' - Write-Host " $currentTask" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/policyDefinitions?api-version=2021-06-01&`$filter=policyType eq 'BuiltIn'" - $method = 'GET' - $requestPolicyDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - Write-Host " $($requestPolicyDefinitionAPI.Count) built-in Policy definitions returned" - $builtinPolicyDefinitions = $requestPolicyDefinitionAPI.where( { $_.properties.policyType -eq 'BuiltIn' } ) - - foreach ($builtinPolicyDefinition in $builtinPolicyDefinitions) { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()) = @{} - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Id = ($builtinPolicyDefinition.Id).ToLower() - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ScopeMGLevel = '' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Scope = 'n/a' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ScopeMgSub = 'n/a' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ScopeId = 'n/a' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).DisplayName = $builtinPolicyDefinition.Properties.displayname - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Name = $builtinPolicyDefinition.Name - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Description = $builtinPolicyDefinition.Properties.description - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Type = $builtinPolicyDefinition.Properties.policyType - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Category = $builtinPolicyDefinition.Properties.metadata.category - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Version = $builtinPolicyDefinition.Properties.metadata.version - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).PolicyDefinitionId = ($builtinPolicyDefinition.Id).ToLower() - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).LinkToAzAdvertizer = "$($builtinPolicyDefinition.Properties.displayname)" - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZ = $false - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZState = '' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZLatestVer = '' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZIdentificationLevel = '' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZPolicyName = '' - if ($builtinPolicyDefinition.Properties.metadata.deprecated -eq $true -or $builtinPolicyDefinition.Properties.displayname -like "``[Deprecated``]*") { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Deprecated = $builtinPolicyDefinition.Properties.metadata.deprecated - } - else { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Deprecated = $false - } - if ($builtinPolicyDefinition.Properties.metadata.preview -eq $true -or $builtinPolicyDefinition.Properties.displayname -like "``[*Preview``]*") { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Preview = $builtinPolicyDefinition.Properties.metadata.preview - } - else { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Preview = $false - } - #region effect - $htEffectDetected = detectPolicyEffect -policyDefinition $builtinPolicyDefinition - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectDefaultValue = $htEffectDetected.defaultValue - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = $htEffectDetected.allowedValues - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectFixedValue = $htEffectDetected.fixedValue - #endregion effect - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Json = $builtinPolicyDefinition - - if (-not [string]::IsNullOrWhiteSpace($builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds)) { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).RoleDefinitionIds = $builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds - foreach ($roledefinitionId in $builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { - if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$builtinPolicyDefinition.Id - } - else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $builtinPolicyDefinition.Id - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies - } - } - } - else { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).RoleDefinitionIds = 'n/a' - } - - #hashes for parity builtin/custom - # $script:htHashesBuiltInPolicy.(($builtinPolicyDefinition.Id).ToLower()) = @{ - # policyRuleHash = getPolicyHash -object ($builtinPolicyDefinition.properties.policyRule | ConvertTo-Json -Depth 99) - # } - $policyRuleHash = (getPolicyHash -json ($builtinPolicyDefinition.properties.policyRule | ConvertTo-Json -Depth 99)) - if (-not $htHashesBuiltInPolicy.($policyRuleHash)) { - $script:htHashesBuiltInPolicy.($policyRuleHash) = @{ - Policies = [System.Collections.ArrayList]@() - } - $null = $script:htHashesBuiltInPolicy.($policyRuleHash).Policies.Add(($builtinPolicyDefinition.Id).ToLower()) - } - else { - #Write-Host "$($builtinPolicyDefinition.name) $($policyRuleHash) already exists" - $null = $script:htHashesBuiltInPolicy.($policyRuleHash).Policies.Add(($builtinPolicyDefinition.Id).ToLower()) - #$htHashesBuiltInPolicy.($policyRuleHash).Policies.Count - } - } - Write-Host " $($htHashesBuiltInPolicy.Keys.Count) unique Policy rule hashes for built-in Policy definitions" - } - - if ($builtInCapability -eq 'PolicyDefinitionsStatic') { - $currentTask = 'Caching static Policy definitions' - Write-Host " $currentTask" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/policyDefinitions?api-version=2021-06-01&`$filter=policyType eq 'Static'" - $method = 'GET' - $requestPolicyDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - Write-Host " $($requestPolicyDefinitionAPI.Count) static Policy definitions returned" - $staticPolicyDefinitions = $requestPolicyDefinitionAPI.where( { $_.properties.policyType -eq 'Static' } ) - - foreach ($staticPolicyDefinition in $staticPolicyDefinitions) { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()) = @{} - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Id = ($staticPolicyDefinition.Id).ToLower() - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ScopeMGLevel = '' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Scope = 'n/a' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ScopeMgSub = 'n/a' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ScopeId = 'n/a' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).DisplayName = $staticPolicyDefinition.Properties.displayname - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Name = $staticPolicyDefinition.Name - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Description = $staticPolicyDefinition.Properties.description - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Type = $staticPolicyDefinition.Properties.policyType - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Category = $staticPolicyDefinition.Properties.metadata.category - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Version = $staticPolicyDefinition.Properties.metadata.version - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).PolicyDefinitionId = ($staticPolicyDefinition.Id).ToLower() - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).LinkToAzAdvertizer = "$($staticPolicyDefinition.Properties.displayname)" - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZ = $false - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZState = '' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZLatestVer = '' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZIdentificationLevel = '' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZPolicyName = '' - - if ($staticPolicyDefinition.Properties.metadata.deprecated -eq $true -or $staticPolicyDefinition.Properties.displayname -like "``[Deprecated``]*") { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Deprecated = $staticPolicyDefinition.Properties.metadata.deprecated - } - else { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Deprecated = $false - } - if ($staticPolicyDefinition.Properties.metadata.preview -eq $true -or $staticPolicyDefinition.Properties.displayname -like "``[*Preview``]*") { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Preview = $staticPolicyDefinition.Properties.metadata.preview - } - else { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Preview = $false - } - #region effect - $htEffectDetected = detectPolicyEffect -policyDefinition $staticPolicyDefinition - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectDefaultValue = $htEffectDetected.defaultValue - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = $htEffectDetected.allowedValues - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectFixedValue = $htEffectDetected.fixedValue - #endregion effect - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Json = $staticPolicyDefinition - - if (-not [string]::IsNullOrWhiteSpace($staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds)) { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).RoleDefinitionIds = $staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds - foreach ($roledefinitionId in $staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { - if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$staticPolicyDefinition.Id - } - else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $staticPolicyDefinition.Id - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies - } - } - } - else { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).RoleDefinitionIds = 'n/a' - } - } - } - - if ($builtInCapability -eq 'PolicySetDefinitions') { - - $currentTask = 'Caching built-in PolicySet definitions' - Write-Host " $currentTask" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/policySetDefinitions?api-version=2021-06-01&`$filter=policyType eq 'BuiltIn'" - $method = 'GET' - $requestPolicySetDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - $builtinPolicySetDefinitions = $requestPolicySetDefinitionAPI.where( { $_.properties.policyType -eq 'BuiltIn' } ) - Write-Host " $($requestPolicySetDefinitionAPI.Count) built-in PolicySet definitions returned" - foreach ($builtinPolicySetDefinition in $builtinPolicySetDefinitions) { - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()) = @{} - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Id = ($builtinPolicySetDefinition.Id).ToLower() - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ScopeMGLevel = '' - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Scope = 'n/a' - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ScopeMgSub = 'n/a' - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ScopeId = 'n/a' - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).DisplayName = $builtinPolicySetDefinition.Properties.displayname - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Name = $builtinPolicySetDefinition.Name - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Description = $builtinPolicySetDefinition.Properties.description - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Type = $builtinPolicySetDefinition.Properties.policyType - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Category = $builtinPolicySetDefinition.Properties.metadata.category - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Version = $builtinPolicySetDefinition.Properties.metadata.version - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).PolicyDefinitionId = ($builtinPolicySetDefinition.Id).ToLower() - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).LinkToAzAdvertizer = "$($builtinPolicySetDefinition.Properties.displayname)" - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZ = $false - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZState = '' - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZLatestVer = '' - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZIdentificationLevel = '' - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZPolicySetName = '' - $arrayPolicySetPolicyIdsToLower = @() - $htPolicySetPolicyRefIds = @{} - $arrayPolicySetPolicyIdsToLower = foreach ($policySetPolicy in $builtinPolicySetDefinition.properties.policydefinitions) { - ($policySetPolicy.policyDefinitionId).ToLower() - $htPolicySetPolicyRefIds.($policySetPolicy.policyDefinitionReferenceId) = ($policySetPolicy.policyDefinitionId) - } - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).PolicySetPolicyIds = $arrayPolicySetPolicyIdsToLower - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).PolicySetPolicyRefIds = $htPolicySetPolicyRefIds - if ($builtinPolicySetDefinition.Properties.metadata.deprecated -eq $true -or $builtinPolicySetDefinition.Properties.displayname -like "``[Deprecated``]*") { - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Deprecated = $builtinPolicySetDefinition.Properties.metadata.deprecated - } - else { - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Deprecated = $false - } - if ($builtinPolicySetDefinition.Properties.metadata.preview -eq $true -or $builtinPolicySetDefinition.Properties.displayname -like "``[*Preview``]*") { - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Preview = $builtinPolicySetDefinition.Properties.metadata.preview - } - else { - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Preview = $false - } - $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Json = $builtinPolicySetDefinition - } - } - - if ($builtInCapability -eq 'RoleDefinitions') { - #Write-Host "`$ignoreARMLocation = '$ignoreARMLocation'" -ForegroundColor Yellow - if ($ignoreARMLocation) { - $currentTask = 'Caching built-in Role definitions' - Write-Host " $currentTask" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].'ARM')/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - } - else { - $currentTask = "Caching built-in Role definitions (Location: '$($ARMLocation)')" - Write-Host " $currentTask" - $uri = "$($azAPICallConf['azAPIEndpointUrls']."ARM$($ARMLocation)")/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - } - - $method = 'GET' - $requestRoleDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - Write-Host " $($requestRoleDefinitionAPI.Count) built-in Role definitions returned" - foreach ($roleDefinition in $requestRoleDefinitionAPI) { - if ( - ( - $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/roleassignments/write' -or - $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/roleassignments/*' -or - $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/*/write' -or - $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/*' -or - $roleDefinition.properties.permissions.actions -contains '*/write' -or - $roleDefinition.properties.permissions.actions -contains '*' - ) -and ( - $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/roleassignments/write' -and - $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/roleassignments/*' -and - $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/*/write' -and - $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/*' -and - $roleDefinition.properties.permissions.notActions -notcontains '*/write' -and - $roleDefinition.properties.permissions.notActions -notcontains '*' - ) - ) { - $roleCapable4RoleAssignmentsWrite = $true - } - else { - $roleCapable4RoleAssignmentsWrite = $false - } - - ($script:htCacheDefinitionsRole).($roleDefinition.name) = @{} - ($script:htCacheDefinitionsRole).($roleDefinition.name).Id = ($roleDefinition.name) - ($script:htCacheDefinitionsRole).($roleDefinition.name).Name = ($roleDefinition.properties.roleName) - ($script:htCacheDefinitionsRole).($roleDefinition.name).IsCustom = $false - ($script:htCacheDefinitionsRole).($roleDefinition.name).AssignableScopes = ($roleDefinition.properties.assignableScopes) - ($script:htCacheDefinitionsRole).($roleDefinition.name).Actions = ($roleDefinition.properties.permissions.actions) - ($script:htCacheDefinitionsRole).($roleDefinition.name).NotActions = ($roleDefinition.properties.permissions.notActions) - ($script:htCacheDefinitionsRole).($roleDefinition.name).DataActions = ($roleDefinition.properties.permissions.dataActions) - ($script:htCacheDefinitionsRole).($roleDefinition.name).NotDataActions = ($roleDefinition.properties.permissions.notDataActions) - ($script:htCacheDefinitionsRole).($roleDefinition.name).Json = ($roleDefinition.properties) - ($script:htCacheDefinitionsRole).($roleDefinition.name).LinkToAzAdvertizer = "$($roleDefinition.properties.roleName)" - ($script:htCacheDefinitionsRole).($roleDefinition.name).RoleCanDoRoleAssignments = $roleCapable4RoleAssignmentsWrite - } - } - } - - $script:builtInPolicyDefinitionsCount = $htCacheDefinitionsPolicy.Values.where({ $_.Type -eq 'BuiltIn' }).count - - $endDefinitionsCaching = Get-Date - Write-Host "Caching built-in definitions duration: $((New-TimeSpan -Start $startDefinitionsCaching -End $endDefinitionsCaching).TotalSeconds) seconds" -} -function checkAzGovVizVersion { - try { - $getRepoVersion = Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/master/version.json' - $repoVersion = ($getRepoVersion.Content | ConvertFrom-Json).ProductVersion - - $script:azGovVizNewerVersionAvailable = $false - if ($repoVersion -ne $ProductVersion) { - $repoVersionSplit = $repoVersion -split '\.' - $repoVersionMajor = $repoVersionSplit[0] - $repoVersionMinor = $repoVersionSplit[1] - $repoVersionPatch = $repoVersionSplit[2] - - $ProductVersionSplit = $ProductVersion -split '\.' - $ProductVersionMajor = $ProductVersionSplit[0] - $ProductVersionMinor = $ProductVersionSplit[1] - $ProductVersionPatch = $ProductVersionSplit[2] - - if ($repoVersionMajor -ne $ProductVersionMajor) { - $versionDrift = 'major' - } - elseif ($repoVersionMinor -ne $ProductVersionMinor) { - $versionDrift = 'minor' - } - elseif ($repoVersionPatch -ne $ProductVersionPatch) { - $versionDrift = 'patch' - } - else { - $versionDrift = 'unknown' - } - - $versionDriftSummary = "$repoVersion ($versionDrift)" - $script:azGovVizVersionOnRepositoryFull = $versionDriftSummary - $script:azGovVizNewerVersionAvailable = $true - $script:azGovVizNewerVersionAvailableHTML = 'Get the latest Azure Governance Visualizer version ' + $azGovVizVersionOnRepositoryFull + '! ' - } - else { - Write-Host "Azure Governance Visualizer version is up to date '$ProductVersion'" -ForegroundColor Green - } - } - catch { - #skip - Write-Host 'Azure Governance Visualizer version check skipped' -ForegroundColor Magenta - } -} -function createTagList { - $startTagListArray = Get-Date - Write-Host 'Creating TagList array' - - $tagsSubRgResCount = ($htAllTagList.'AllScopes'.Keys).Count - $tagsSubsriptionCount = ($htAllTagList.'Subscription'.Keys).Count - $tagsResourceGroupCount = ($htAllTagList.'ResourceGroup'.Keys).Count - $tagsResourceCount = ($htAllTagList.'Resource'.Keys).Count - Write-Host " Total Number of ALL unique Tag Names: $tagsSubRgResCount" - Write-Host " Total Number of Subscription unique Tag Names: $tagsSubsriptionCount" - Write-Host " Total Number of ResourceGroup unique Tag Names: $tagsResourceGroupCount" - Write-Host " Total Number of Resource unique Tag Names: $tagsResourceCount" - - foreach ($tagScope in $htAllTagList.keys) { - foreach ($tagScopeTagName in $htAllTagList.($tagScope).keys) { - $null = $script:arrayTagList.Add([PSCustomObject]@{ - Scope = $tagScope - TagName = ($tagScopeTagName) - TagCount = $htAllTagList.($tagScope).($tagScopeTagName) - }) - } - } - $endTagListArray = Get-Date - Write-Host "Creating TagList array duration: $((New-TimeSpan -Start $startTagListArray -End $endTagListArray).TotalMinutes) minutes ($((New-TimeSpan -Start $startTagListArray -End $endTagListArray).TotalSeconds) seconds)" -} -function detailSubscriptions { - $start = Get-Date - Write-Host 'Subscription picking' - #API in rare cases returns duplicates, therefor sorting unique (id) - $childrenSubscriptions = $arrayEntitiesFromAPI.where( { $_.properties.parentNameChain -contains $ManagementGroupID -and $_.type -eq '/subscriptions' } ) | Sort-Object -Property id -Unique - $script:childrenSubscriptionsCount = ($childrenSubscriptions).Count - $script:subsToProcessInCustomDataCollection = [System.Collections.ArrayList]@() - - if ($htSubscriptionsFromOtherTenants.keys.count -gt 0) { - foreach ($subscriptionExludedOtherTenant in $htSubscriptionsFromOtherTenants.keys) { - $subscriptionExludedOtherTenantDetail = $htSubscriptionsFromOtherTenants.($subscriptionExludedOtherTenant).subDetails - $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ - subscriptionId = $subscriptionExludedOtherTenantDetail.subscriptionId - subscriptionName = $subscriptionExludedOtherTenantDetail.displayName - outOfScopeReason = "Foreign tenant: Id: $($subscriptionExludedOtherTenantDetail.tenantId)" - ManagementGroupId = '' - ManagementGroupName = '' - Level = '' - }) - } - } - - if ($htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.keys.count -gt 0) { - foreach ($subscriptionExludedInEntitiesNotInSubscriptions in $htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.keys) { - $subscriptionExludedInEntitiesNotInSubscriptionsDetail = $htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($subscriptionExludedInEntitiesNotInSubscriptions) - $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ - subscriptionId = $subscriptionExludedInEntitiesNotInSubscriptions - subscriptionName = $subscriptionExludedInEntitiesNotInSubscriptionsDetail.properties.displayName - outOfScopeReason = 'Sub in GetEntities, not in GetSubscriptions' - ManagementGroupId = '' - ManagementGroupName = '' - Level = '' - }) - } - } - - foreach ($childrenSubscription in $childrenSubscriptions) { - - $sub = $htAllSubscriptionsFromAPI.($childrenSubscription.name) - if ($sub.subDetails.subscriptionPolicies.quotaId.startswith('AAD_', 'CurrentCultureIgnoreCase') -or $sub.subDetails.state -ne 'Enabled') { - if (($sub.subDetails.subscriptionPolicies.quotaId).startswith('AAD_', 'CurrentCultureIgnoreCase')) { - $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ - subscriptionId = $childrenSubscription.name - subscriptionName = $childrenSubscription.properties.displayName - outOfScopeReason = "QuotaId: AAD_ (State: $($sub.subDetails.state))" - ManagementGroupId = $htSubscriptionsMgPath.($childrenSubscription.name).Parent - ManagementGroupName = $htSubscriptionsMgPath.($childrenSubscription.name).ParentName - Level = $htSubscriptionsMgPath.($childrenSubscription.name).level - }) - } - if ($sub.subDetails.state -ne 'Enabled') { - $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ - subscriptionId = $childrenSubscription.name - subscriptionName = $childrenSubscription.properties.displayName - outOfScopeReason = "State: $($sub.subDetails.state)" - ManagementGroupId = $htSubscriptionsMgPath.($childrenSubscription.name).Parent - ManagementGroupName = $htSubscriptionsMgPath.($childrenSubscription.name).ParentName - Level = $htSubscriptionsMgPath.($childrenSubscription.name).level - }) - } - } - else { - if ($SubscriptionQuotaIdWhitelist[0] -ne 'undefined') { - $whitelistMatched = 'unknown' - foreach ($subscriptionQuotaIdWhitelistQuotaId in $SubscriptionQuotaIdWhitelist) { - if (($sub.subDetails.subscriptionPolicies.quotaId).startswith($subscriptionQuotaIdWhitelistQuotaId, 'CurrentCultureIgnoreCase')) { - $whitelistMatched = 'inWhitelist' - } - } - - if ($whitelistMatched -eq 'inWhitelist') { - #write-host "$($childrenSubscription.properties.displayName) in whitelist" - $null = $script:subsToProcessInCustomDataCollection.Add([PSCustomObject]@{ - subscriptionId = $childrenSubscription.name - subscriptionName = $childrenSubscription.properties.displayName - subscriptionQuotaId = $sub.subDetails.subscriptionPolicies.quotaId - }) - } - else { - #Write-Host " preCustomDataCollection: $($childrenSubscription.properties.displayName) ($($childrenSubscription.name)) Subscription Quota Id: $($sub.subDetails.subscriptionPolicies.quotaId) is out of scope for Azure Governance Visualizer (not in Whitelist)" - $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ - subscriptionId = $childrenSubscription.name - subscriptionName = $childrenSubscription.properties.displayName - outOfScopeReason = "QuotaId: '$($sub.subDetails.subscriptionPolicies.quotaId)' not in Whitelist" - ManagementGroupId = $htSubscriptionsMgPath.($childrenSubscription.name).Parent - ManagementGroupName = $htSubscriptionsMgPath.($childrenSubscription.name).ParentName - Level = $htSubscriptionsMgPath.($childrenSubscription.name).level - }) - } - } - else { - $null = $script:subsToProcessInCustomDataCollection.Add([PSCustomObject]@{ - subscriptionId = $childrenSubscription.name - subscriptionName = $childrenSubscription.properties.displayName - subscriptionQuotaId = $sub.subDetails.subscriptionPolicies.quotaId - }) - } - } - } - - if ($subsToProcessInCustomDataCollection.Count -lt $childrenSubscriptionsCount) { - Write-Host " $($subsToProcessInCustomDataCollection.Count) of $($childrenSubscriptionsCount) Subscriptions picked for processing" -ForegroundColor yellow - } - else { - Write-Host " $($subsToProcessInCustomDataCollection.Count) of $($childrenSubscriptionsCount) Subscriptions picked for processing" - } - - - if ($outOfScopeSubscriptions.Count -gt 0) { - Write-Host " $($outOfScopeSubscriptions.Count) Subscriptions excluded" -ForegroundColor yellow - $outOfScopeSubscriptionsGroupedByOutOfScopeReason = $outOfScopeSubscriptions | Group-Object -Property outOfScopeReason - foreach ($exclusionreason in $outOfScopeSubscriptionsGroupedByOutOfScopeReason) { - Write-Host " $($exclusionreason.Count): $($exclusionreason.Name) ($($exclusionreason.Group.subscriptionId -join ', '))" - } - - foreach ($outOfScopeSubscription in $outOfScopeSubscriptions) { - $script:htOutOfScopeSubscriptions.($outOfScopeSubscription.subscriptionId) = @{ - subscriptionId = $outOfScopeSubscription.subscriptionId - subscriptionName = $outOfScopeSubscription.subscriptionName - outOfScopeReason = $outOfScopeSubscription.outOfScopeReason - ManagementGroupId = $outOfScopeSubscription.ManagementGroupId - ManagementGroupName = $outOfScopeSubscription.ManagementGroupName - Level = $outOfScopeSubscription.Level - } - } - } - else { - Write-Host " $($outOfScopeSubscriptions.Count) Subscriptions excluded" - } - $script:subsToProcessInCustomDataCollectionCount = ($subsToProcessInCustomDataCollection).Count - - $end = Get-Date - Write-Host "Subscription picking duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" -} -function detectPolicyEffect { - [CmdletBinding()] - Param - ( - [object] - $policyDefinition - ) - - $htEffect = @{ - defaultValue = 'n/a' - allowedValues = 'n/a' - fixedValue = 'n/a' - } - if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.policyRule.then.effect)) { - if ($policyDefinition.properties.policyRule.then.effect -in $ValidPolicyEffects) { - # $arrayeffect += "fixed: $($policyDefinition.properties.policyRule.then.effect)" - # return $arrayeffect - $htEffect.fixedValue = $policyDefinition.properties.policyRule.then.effect - return $htEffect - } - else { - $Regex = [Regex]::new("(?<=\[parameters\(')(.*)(?='\)\])") - $Match = $Regex.Match($policyDefinition.properties.policyRule.then.effect) - if ($Match.Success) { - if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value))) { - - #defaultValue - if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'defaultvalue') { - if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).defaultValue)) { - if ($policyDefinition.properties.parameters.($Match.Value).defaultValue -in $ValidPolicyEffects) { - #$arrayeffect += "default: $($policyDefinition.properties.parameters.($Match.Value).defaultValue)" - $htEffect.defaultValue = $policyDefinition.properties.parameters.($Match.Value).defaultValue - } - else { - Write-Host "invalid defaultValue effect $($policyDefinition.properties.parameters.($Match.Value).defaultValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - } - } - else { - Write-Host "defaultValue empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - } - } - else { - Write-Host "finding: Policy has no defaultvalue for effect: $($policyDefinition.id) ($($policyDefinition.properties.policyType))" - } - #allowedValues - if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'allowedValues') { - if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).allowedValues)) { - if ($policyDefinition.properties.parameters.($Match.Value).allowedValues.Count -gt 0) { - #Write-Host "allowedValues count $($policyDefinition.properties.parameters.($Match.Value).allowedValues) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - $arrayAllowed = @() - foreach ($allowedValue in $policyDefinition.properties.parameters.($Match.Value).allowedValues) { - if ($allowedValue -in $ValidPolicyEffects) { - $arrayAllowed += $allowedValue - } - else { - Write-Host "invalid allowedValue effect $($allowedValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - } - } - #$arrayeffect += "allowed: $(($arrayAllowed | Sort-Object) -join ', ')" - $htEffect.allowedValues = ($arrayAllowed | Sort-Object) -join ',' - } - } - else { - Write-Host "allowedValues empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - } - } - else { - Write-Host "no allowedValues- $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - } - - } - else { - Write-Host "unexpected - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - } - - return $htEffect - } - } - } - else { - Write-Host "no then effect - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" - } - return $htEffect -} -function exportBaseCSV { - if (-not $NoCsvExport) { - Write-Host "Exporting CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName).csv'" - $startBuildCSV = Get-Date - - $outprops = $newtable[0].PSObject.Properties.Name - if (-not $HierarchyMapOnly -and -not $HierarchyMapOnlyCustomDataJSON) { - $outprops.Set($outprops.IndexOf('PolicyAssignmentNotScopes'), @{L = 'PolicyAssignmentNotScopes'; E = { ($_.PolicyAssignmentNotScopes -join "$CsvDelimiterOpposite ") } }) - } - if ($CsvExportUseQuotesAsNeeded) { - $newTable | Sort-Object -Property level, mgId, SubscriptionId, PolicyAssignmentId, RoleAssignmentId, BlueprintId, BlueprintAssignmentId | Select-Object -Property $outprops -ExcludeProperty PolicyAssignmentParameters | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).csv" -Delimiter "$csvDelimiter" -NoTypeInformation -UseQuotes AsNeeded - } - else { - $newTable | Sort-Object -Property level, mgId, SubscriptionId, PolicyAssignmentId, RoleAssignmentId, BlueprintId, BlueprintAssignmentId | Select-Object -Property $outprops -ExcludeProperty PolicyAssignmentParameters | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).csv" -Delimiter "$csvDelimiter" -NoTypeInformation - } - - $endBuildCSV = Get-Date - Write-Host "Exporting CSV total duration: $((New-TimeSpan -Start $startBuildCSV -End $endBuildCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildCSV -End $endBuildCSV).TotalSeconds) seconds)" - } -} -function exportResourceLocks { - $arrayResourceLocks4CSV = [System.Collections.ArrayList]@() - foreach ($sub in $htResourceLocks.Keys) { - $hlper = $htSubscriptionsMgPath.($sub) - $subscriptionDisplayName = $hlper.DisplayName - $mgPath = $hlper.ParentNameChainDelimited - #sub - if ($htResourceLocks.($sub).SubscriptionLocksCannotDeleteCount -eq 1) { - $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ - SubscriptionId = $sub - SubscriptionName = $subscriptionDisplayName - MGPath = $mgPath - ScopeType = 'Subscription' - Lock = 'CannotDelete' - Id = "/subscriptions/$sub" - ResourceType = 'Microsoft.Resources/subscriptions' - }) - } - if ($htResourceLocks.($sub).SubscriptionLocksReadOnlyCount -eq 1) { - $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ - SubscriptionId = $sub - SubscriptionName = $subscriptionDisplayName - MGPath = $mgPath - ScopeType = 'Subscription' - Lock = 'ReadOnly' - Id = "/subscriptions/$sub" - ResourceType = 'Microsoft.Resources/subscriptions' - }) - } - #rg - if ($htResourceLocks.($sub).ResourceGroupsLocksCannotDeleteCount -gt 0) { - foreach ($res in $htResourceLocks.($sub).ResourceGroupsLocksCannotDelete) { - $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ - SubscriptionId = $sub - SubscriptionName = $subscriptionDisplayName - MGPath = $mgPath - ScopeType = 'ResourceGroup' - Lock = 'CannotDelete' - Id = $res.rg - ResourceType = 'Microsoft.Resources/subscriptions/resourceGroups' - }) - } - } - if ($htResourceLocks.($sub).ResourceGroupsLocksReadOnlyCount -gt 0) { - foreach ($res in $htResourceLocks.($sub).ResourceGroupsLocksReadOnly) { - $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ - SubscriptionId = $sub - SubscriptionName = $subscriptionDisplayName - MGPath = $mgPath - ScopeType = 'ResourceGroup' - Lock = 'ReadOnly' - Id = $res.rg - ResourceType = 'Microsoft.Resources/subscriptions/resourceGroups' - }) - } - } - #res - if ($htResourceLocks.($sub).ResourcesLocksCannotDeleteCount -gt 0) { - foreach ($res in $htResourceLocks.($sub).ResourcesLocksCannotDelete) { - $resSplit = ($res.res -split '/') - $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ - SubscriptionId = $sub - SubscriptionName = $subscriptionDisplayName - MGPath = $mgPath - ScopeType = 'Resource' - Lock = 'CannotDelete' - Id = $res.res - ResourceType = "$($resSplit[6])/$($resSplit[7])" - }) - } - } - if ($htResourceLocks.($sub).ResourcesLocksReadOnlyCount -gt 0) { - foreach ($res in $htResourceLocks.($sub).ResourcesLocksReadOnly) { - $resSplit = ($res.res -split '/') - $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ - SubscriptionId = $sub - SubscriptionName = $subscriptionDisplayName - MGPath = $mgPath - ScopeType = 'Resource' - Lock = 'ReadOnly' - Id = $res.res - ResourceType = "$($resSplit[6])/$($resSplit[7])" - }) - } - } - } - if ($arrayResourceLocks4CSV.count -gt 0) { - if (-not $NoCsvExport) { - Write-Host "Exporting ResourceLocks CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceLocks.csv'" - $arrayResourceLocks4CSV | Sort-Object -Property ScopeType, Lock, SubscriptionId, Id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceLocks.csv" -Delimiter "$csvDelimiter" -NoTypeInformation - } - } -} -function getConsumption { - - function addToAllConsumptionData { - [CmdletBinding()]Param( - [Parameter(Mandatory)] - [object] - $consumptiondataFromAPI - ) - - foreach ($consumptionline in $consumptiondataFromAPI.properties.rows) { - $hlper = $htSubscriptionsMgPath.($consumptionline[1]) - - $null = $script:allConsumptionData.Add([PSCustomObject]@{ - "$($consumptiondataFromAPI.properties.columns.name[0])" = [decimal]$consumptionline[0] - "$($consumptiondataFromAPI.properties.columns.name[1])" = $consumptionline[1] - SubscriptionName = $hlper.DisplayName - SubscriptionMgPath = $hlper.ParentNameChainDelimited - "$($consumptiondataFromAPI.properties.columns.name[2])" = $consumptionline[2] - "$($consumptiondataFromAPI.properties.columns.name[3])" = $consumptionline[3] - "$($consumptiondataFromAPI.properties.columns.name[4])" = $consumptionline[4] - "$($consumptiondataFromAPI.properties.columns.name[5])" = $consumptionline[5] - "$($consumptiondataFromAPI.properties.columns.name[6])" = $consumptionline[6] - }) - } - } - - $startConsumptionData = Get-Date - - #cost only for whitelisted quotaId - if ($SubscriptionQuotaIdWhitelist[0] -ne 'undefined') { - if ($subsToProcessInCustomDataCollectionCount -gt 0) { - #region mgScopeWhitelisted - #$subscriptionIdsOptimizedForBody = '"{0}"' -f ($subsToProcessInCustomDataCollection.subscriptionId -join '","') - $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for $($subsToProcessInCustomDataCollectionCount) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" - Write-Host "$currentTask" - #https://learn.microsoft.com/rest/api/cost-management/query/usage - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" - $method = 'POST' - - $counterBatch = [PSCustomObject] @{ Value = 0 } - $batchSize = 100 - $subscriptionsBatch = ($subsToProcessInCustomDataCollection | Sort-Object -Property subscriptionQuotaId) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } - $batchCnt = 0 - - foreach ($batch in $subscriptionsBatch) { - $batchCnt++ - $subscriptionIdsOptimizedForBody = '"{0}"' -f (($batch.Group).subscriptionId -join '","') - $currenttask = "Getting Consumption data #batch$($batchCnt)/$(($subscriptionsBatch | Measure-Object).Count) (scope MG '$($ManagementGroupId)') for $(($batch.Group).Count) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" - Write-Host "$currentTask" -ForegroundColor Cyan - - $body = @" -{ -"type": "ActualCost", -"dataset": { - "granularity": "none", - "filter": { - "dimensions": { - "name": "SubscriptionId", - "operator": "In", - "values": [ - $($subscriptionIdsOptimizedForBody) - ] - } - }, - "aggregation": { - "totalCost": { - "name": "PreTaxCost", - "function": "Sum" - } - }, - "grouping": [ - { - "type": "Dimension", - "name": "SubscriptionId" - }, - { - "type": "Dimension", - "name": "ResourceId" - }, - { - "type": "Dimension", - "name": "ResourceType" - }, - { - "type": "Dimension", - "name": "MeterCategory" - }, - { - "type": "Dimension", - "name": "ChargeType" - } - ] -}, -"timeframe": "Custom", -"timeperiod": { - "from": "$($azureConsumptionStartDate)", - "to": "$($azureConsumptionEndDate)" -} -} -"@ - - $mgConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' - #endregion mgScopeWhitelisted - - <#test - #$mgConsumptionData = "OfferNotSupported" - if ($batchCnt -eq 1){ - $mgConsumptionData = "OfferNotSupported" - } - #> - - if ($mgConsumptionData -eq 'Unauthorized' -or $mgConsumptionData -eq 'OfferNotSupported' -or $mgConsumptionData -eq 'NoValidSubscriptions') { - if (-not $script:htConsumptionExceptionLog.Mg.($ManagementGroupId)) { - $script:htConsumptionExceptionLog.Mg.($ManagementGroupId) = @{} - } - $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).($batchCnt) = @{} - $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).($batchCnt).Exception = $mgConsumptionData - $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).($batchCnt).Subscriptions = ($batch.Group).subscriptionId - Write-Host " Switching to 'foreach Subscription' Subscription scope mode. Getting Consumption data #batch$($batchCnt) using Management Group scope failed." - #region subScopewhitelisted - $body = @" -{ -"type": "ActualCost", -"dataset": { - "granularity": "none", - "aggregation": { - "totalCost": { - "name": "PreTaxCost", - "function": "Sum" - } - }, - "grouping": [ - { - "type": "Dimension", - "name": "SubscriptionId" - }, - { - "type": "Dimension", - "name": "ResourceId" - }, - { - "type": "Dimension", - "name": "ResourceType" - }, - { - "type": "Dimension", - "name": "MeterCategory" - }, - { - "type": "Dimension", - "name": "ChargeType" - } - ] -}, -"timeframe": "Custom", -"timeperiod": { - "from": "$($azureConsumptionStartDate)", - "to": "$($azureConsumptionEndDate)" -} -} -"@ - $funcAddToAllConsumptionData = $function:addToAllConsumptionData.ToString() - $batch.Group | ForEach-Object -Parallel { - $subIdToProcess = $_.subscriptionId - $subNameToProcess = $_.subscriptionName - $subscriptionQuotaIdToProcess = $_.subscriptionQuotaId - #region UsingVARs - $body = $using:body - $azureConsumptionStartDate = $using:azureConsumptionStartDate - $azureConsumptionEndDate = $using:azureConsumptionEndDate - $SubscriptionQuotaIdWhitelist = $using:SubscriptionQuotaIdWhitelist - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $allConsumptionData = $using:allConsumptionData - $htSubscriptionsMgPath = $using:htSubscriptionsMgPath - $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI - $htConsumptionExceptionLog = $using:htConsumptionExceptionLog - #other - $function:addToAllConsumptionData = $using:funcAddToAllConsumptionData - #endregion UsingVARs - - $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" - #test - Write-Host $currentTask - #https://learn.microsoft.com/rest/api/cost-management/query/usage - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" - $method = 'POST' - $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' - if ($subConsumptionData -eq 'Unauthorized' -or $subConsumptionData -eq 'OfferNotSupported' -or $subConsumptionData -eq 'InvalidQueryDefinition' -or $subConsumptionData -eq 'NonValidWebDirectAIRSOfferType' -or $subConsumptionData -eq 'NotFoundNotSupported' -or $subConsumptionData -eq 'IndirectCostDisabled') { - Write-Host " Failed ($subConsumptionData) - Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" - $hlper = $htAllSubscriptionsFromAPI.($subIdToProcess).subDetails - $hlper2 = $htSubscriptionsMgPath.($subIdToProcess) - $script:htConsumptionExceptionLog.Sub.($subIdToProcess) = @{} - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).Exception = $subConsumptionData - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionId = $subIdToProcess - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionName = $hlper.displayName - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).QuotaId = $hlper.subscriptionPolicies.quotaId - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgPath = $hlper2.ParentNameChainDelimited - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgParent = $hlper2.Parent - Continue - } - else { - Write-Host " $($subConsumptionData.Count) Consumption data entries ((scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess))))" - if ($subConsumptionData.Count -gt 0) { - addToAllConsumptionData -consumptiondataFromAPI $subConsumptionData - <# - foreach ($consumptionEntry in $subConsumptionData) { - if ($consumptionEntry.PreTaxCost -ne 0) { - $null = $script:allConsumptionData.Add($consumptionEntry) - } - } - #> - - } - } - } -ThrottleLimit $ThrottleLimit - #endregion subScopewhitelisted - } - else { - Write-Host " $($mgConsumptionData.Count) Consumption data entries" - if ($mgConsumptionData.Count -gt 0) { - addToAllConsumptionData -consumptiondataFromAPI $mgConsumptionData - <# - foreach ($consumptionEntry in $mgConsumptionData) { - if ($consumptionEntry.PreTaxCost -ne 0) { - $null = $script:allConsumptionData.Add($consumptionEntry) - } - } - #> - } - } - } - } - else { - $detailShowStopperResult = 'NoWhitelistSubscriptionsPresent' - Write-Host ' No Subscriptions matching whitelist present, skipping Consumption data processing' - #überprüfen - } - } - else { - - if ($subsToProcessInCustomDataCollectionCount -gt 0) { - #region mgScope - $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" - Write-Host "$currentTask" - #https://learn.microsoft.com/rest/api/cost-management/query/usage - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" - $method = 'POST' - $body = @" -{ - "type": "ActualCost", - "dataset": { - "granularity": "none", - "aggregation": { - "totalCost": { - "name": "PreTaxCost", - "function": "Sum" - } - }, - "grouping": [ - { - "type": "Dimension", - "name": "SubscriptionId" - }, - { - "type": "Dimension", - "name": "ResourceId" - }, - { - "type": "Dimension", - "name": "ResourceType" - }, - { - "type": "Dimension", - "name": "MeterCategory" - }, - { - "type": "Dimension", - "name": "ChargeType" - } - ] - }, - "timeframe": "Custom", - "timeperiod": { - "from": "$($azureConsumptionStartDate)", - "to": "$($azureConsumptionEndDate)" - } -} -"@ - #$script:allConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' - $allConsumptionDataAPIResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' - #endregion mgScope - - #test - #$allConsumptionData = "OfferNotSupported" - - if ($allConsumptionDataAPIResult -eq 'AccountCostDisabled' <#-or $allConsumptionDataAPIResult -eq 'NoValidSubscriptions'#>) { - if ($allConsumptionDataAPIResult -eq 'AccountCostDisabled') { - $detailShowStopperResult = $allConsumptionDataAPIResult - } - <#if ($allConsumptionDataAPIResult -eq 'NoValidSubscriptions') { - $detailShowStopperResult = $allConsumptionDataAPIResult - }#> - } - else { - if ($allConsumptionDataAPIResult -eq 'Unauthorized' -or $allConsumptionDataAPIResult -eq 'OfferNotSupported' -or $allConsumptionDataAPIResult -eq 'NoValidSubscriptions' -or $allConsumptionDataAPIResult -eq 'tooManySubscriptions') { - $script:htConsumptionExceptionLog.Mg.($ManagementGroupId) = @{} - $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).Exception = $allConsumptionDataAPIResult - Write-Host " Switching to 'foreach Subscription' mode. Getting Consumption data using Management Group scope failed." - #region subScope - $body = @" -{ - "type": "ActualCost", - "dataset": { - "granularity": "none", - "aggregation": { - "totalCost": { - "name": "PreTaxCost", - "function": "Sum" - } - }, - "grouping": [ - { - "type": "Dimension", - "name": "SubscriptionId" - }, - { - "type": "Dimension", - "name": "ResourceId" - }, - { - "type": "Dimension", - "name": "ResourceType" - }, - { - "type": "Dimension", - "name": "MeterCategory" - }, - { - "type": "Dimension", - "name": "ChargeType" - } - ] - }, - "timeframe": "Custom", - "timeperiod": { - "from": "$($azureConsumptionStartDate)", - "to": "$($azureConsumptionEndDate)" - } -} -"@ - - $funcAddToAllConsumptionData = $function:addToAllConsumptionData.ToString() - $subsToProcessInCustomDataCollection | ForEach-Object -Parallel { - $subIdToProcess = $_.subscriptionId - $subNameToProcess = $_.subscriptionName - $subscriptionQuotaIdToProcess = $_.subscriptionQuotaId - #region UsingVARs - $body = $using:body - $azureConsumptionStartDate = $using:azureConsumptionStartDate - $azureConsumptionEndDate = $using:azureConsumptionEndDate - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $htSubscriptionsMgPath = $using:htSubscriptionsMgPath - $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI - $allConsumptionData = $using:allConsumptionData - $htConsumptionExceptionLog = $using:htConsumptionExceptionLog - #other - $function:addToAllConsumptionData = $using:funcAddToAllConsumptionData - #endregion UsingVARs - - $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" - #test - Write-Host $currentTask - #https://learn.microsoft.com/rest/api/cost-management/query/usage - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" - $method = 'POST' - $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' - if ($subConsumptionData -eq 'Unauthorized' -or $subConsumptionData -eq 'OfferNotSupported' -or $subConsumptionData -eq 'InvalidQueryDefinition' -or $subConsumptionData -eq 'NonValidWebDirectAIRSOfferType' -or $subConsumptionData -eq 'NotFoundNotSupported' -or $subConsumptionData -eq 'IndirectCostDisabled') { - Write-Host " Failed ($subConsumptionData) - Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" - $hlper = $htAllSubscriptionsFromAPI.($subIdToProcess).subDetails - $hlper2 = $htSubscriptionsMgPath.($subIdToProcess) - $script:htConsumptionExceptionLog.Sub.($subIdToProcess) = @{} - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).Exception = $subConsumptionData - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionId = $subIdToProcess - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionName = $hlper.displayName - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).QuotaId = $hlper.subscriptionPolicies.quotaId - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgPath = $hlper2.ParentNameChainDelimited - $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgParent = $hlper2.Parent - Continue - } - else { - Write-Host " $($subConsumptionData.Count) Consumption data entries (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" - if ($subConsumptionData.Count -gt 0) { - addToAllConsumptionData -consumptiondataFromAPI $subConsumptionData - <# - foreach ($consumptionEntry in $subConsumptionData) { - if ($consumptionEntry.PreTaxCost -ne 0) { - $null = $script:allConsumptionData.Add($consumptionEntry) - } - } - #> - } - } - } -ThrottleLimit $ThrottleLimit - #endregion subScope - } - else { - Write-Host " $($allConsumptionDataAPIResult.properties.rows.Count) Consumption data entries" - if ($allConsumptionDataAPIResult.properties.rows.Count -gt 0) { - addToAllConsumptionData -consumptiondataFromAPI $allConsumptionDataAPIResult - } - } - } - } - else { - $detailShowStopperResult = 'NoSubscriptionsPresent' - Write-Host ' No Subscriptions present, skipping Consumption data processing' - } - } - - if ($detailShowStopperResult -eq 'AccountCostDisabled' -or $detailShowStopperResult -eq 'NoValidSubscriptions' -or $detailShowStopperResult -eq 'NoWhitelistSubscriptionsPresent' -or $detailShowStopperResult -eq 'NoSubscriptionsPresent') { - if ($detailShowStopperResult -eq 'AccountCostDisabled') { - Write-Host ' Seems Access to cost data has been disabled for this Account - skipping CostManagement' - } - if ($detailShowStopperResult -eq 'NoValidSubscriptions') { - Write-Host ' Seems there are no valid Subscriptions present - skipping CostManagement' - } - if ($detailShowStopperResult -eq 'NoWhitelistSubscriptionsPresent') { - Write-Host " Seems there are no Subscriptions present that match the whitelist ($($SubscriptionQuotaIdWhitelist -join ', ')) - skipping CostManagement" - } - if ($detailShowStopperResult -eq 'NoSubscriptionsPresent') { - Write-Host ' Seems there are no Subscriptions present - skipping CostManagement' - } - Write-Host " Action: Setting switch parameter 'DoAzureConsumption' to false" - $azAPICallConf['htParameters'].DoAzureConsumption = $false - } - else { - Write-Host ' Checking returned Consumption data' - $script:allConsumptionDataCount = $allConsumptionData.Count - - if ($allConsumptionDataCount -gt 0) { - - $script:allConsumptionData = $allConsumptionData.where( { $_.PreTaxCost -ne 0 } ) - $script:allConsumptionDataCount = $allConsumptionData.Count - - if ($allConsumptionDataCount -gt 0) { - Write-Host " $($allConsumptionDataCount) relevant Consumption data entries" - - $script:consumptionData = $allConsumptionData - $script:consumptionDataGroupedByCurrency = $consumptionData | Group-Object -Property Currency - - foreach ($currency in $consumptionDataGroupedByCurrency) { - - #subscriptions - $groupAllConsumptionDataPerCurrencyBySubscriptionId = $currency.group | Group-Object -Property SubscriptionId - foreach ($subscriptionId in $groupAllConsumptionDataPerCurrencyBySubscriptionId) { - - $subTotalCost = ($subscriptionId.Group.PreTaxCost | Measure-Object -Sum).Sum - $script:htAzureConsumptionSubscriptions.($subscriptionId.Name) = @{} - $script:htAzureConsumptionSubscriptions.($subscriptionId.Name).ConsumptionData = $subscriptionId.group - $script:htAzureConsumptionSubscriptions.($subscriptionId.Name).TotalCost = $subTotalCost - $script:htAzureConsumptionSubscriptions.($subscriptionId.Name).Currency = $currency.Name - $resourceTypes = $subscriptionId.Group.ResourceType | Sort-Object -Unique - - foreach ($parentMg in $htSubscriptionsMgPath.($subscriptionId.Name).ParentNameChain) { - - if (-not $htManagementGroupsCost.($parentMg)) { - $script:htManagementGroupsCost.($parentMg) = @{} - $script:htManagementGroupsCost.($parentMg).currencies = $currency.Name - $script:htManagementGroupsCost.($parentMg)."mgTotalCost_$($currency.Name)" = $subTotalCost #[decimal]$subTotalCost - $script:htManagementGroupsCost.($parentMg)."resourcesThatGeneratedCost_$($currency.Name)" = ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count - $script:htManagementGroupsCost.($parentMg).resourcesThatGeneratedCostCurrencyIndependent = ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count - $script:htManagementGroupsCost.($parentMg)."subscriptionsThatGeneratedCost_$($currency.Name)" = 1 - $script:htManagementGroupsCost.($parentMg).subscriptionsThatGeneratedCostCurrencyIndependent = 1 - $script:htManagementGroupsCost.($parentMg)."resourceTypesThatGeneratedCost_$($currency.Name)" = $resourceTypes - $script:htManagementGroupsCost.($parentMg).resourceTypesThatGeneratedCostCurrencyIndependent = $resourceTypes - $script:htManagementGroupsCost.($parentMg)."consumptionDataSubscriptions_$($currency.Name)" = $subscriptionId.group - $script:htManagementGroupsCost.($parentMg).consumptionDataSubscriptions = $subscriptionId.group - } - else { - $newMgTotalCost = $htManagementGroupsCost.($parentMg)."mgTotalCost_$($currency.Name)" + $subTotalCost #[decimal]$subTotalCost - $script:htManagementGroupsCost.($parentMg)."mgTotalCost_$($currency.Name)" = $newMgTotalCost #[decimal]$newMgTotalCost - - $currencies = [array]$htManagementGroupsCost.($parentMg).currencies - if ($currencies -notcontains $currency.Name) { - $currencies += $currency.Name - $script:htManagementGroupsCost.($parentMg).currencies = $currencies - } - - #currency based - $resourcesThatGeneratedCost = $htManagementGroupsCost.($parentMg)."resourcesThatGeneratedCost_$($currency.Name)" + ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count - $script:htManagementGroupsCost.($parentMg)."resourcesThatGeneratedCost_$($currency.Name)" = $resourcesThatGeneratedCost - - $subscriptionsThatGeneratedCost = $htManagementGroupsCost.($parentMg)."subscriptionsThatGeneratedCost_$($currency.Name)" + 1 - $script:htManagementGroupsCost.($parentMg)."subscriptionsThatGeneratedCost_$($currency.Name)" = $subscriptionsThatGeneratedCost - - $consumptionDataSubscriptions = $htManagementGroupsCost.($parentMg)."consumptionDataSubscriptions_$($currency.Name)" += $subscriptionId.group - $script:htManagementGroupsCost.($parentMg)."consumptionDataSubscriptions_$($currency.Name)" = $consumptionDataSubscriptions - - $resourceTypesThatGeneratedCost = $htManagementGroupsCost.($parentMg)."resourceTypesThatGeneratedCost_$($currency.Name)" - foreach ($resourceType in $resourceTypes) { - if ($resourceTypesThatGeneratedCost -notcontains $resourceType) { - $resourceTypesThatGeneratedCost += $resourceType - } - } - $script:htManagementGroupsCost.($parentMg)."resourceTypesThatGeneratedCost_$($currency.Name)" = $resourceTypesThatGeneratedCost - - #currencyIndependent - $resourcesThatGeneratedCostCurrencyIndependent = $htManagementGroupsCost.($parentMg).resourcesThatGeneratedCostCurrencyIndependent + ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count - $script:htManagementGroupsCost.($parentMg).resourcesThatGeneratedCostCurrencyIndependent = $resourcesThatGeneratedCostCurrencyIndependent - - $subscriptionsThatGeneratedCostCurrencyIndependent = $htManagementGroupsCost.($parentMg).subscriptionsThatGeneratedCostCurrencyIndependent + 1 - $script:htManagementGroupsCost.($parentMg).subscriptionsThatGeneratedCostCurrencyIndependent = $subscriptionsThatGeneratedCostCurrencyIndependent - - $consumptionDataSubscriptionsCurrencyIndependent = $htManagementGroupsCost.($parentMg).consumptionDataSubscriptions += $subscriptionId.group - $script:htManagementGroupsCost.($parentMg).consumptionDataSubscriptions = $consumptionDataSubscriptionsCurrencyIndependent - - $resourceTypesThatGeneratedCostCurrencyIndependent = $htManagementGroupsCost.($parentMg).resourceTypesThatGeneratedCostCurrencyIndependent - foreach ($resourceType in $resourceTypes) { - if ($resourceTypesThatGeneratedCostCurrencyIndependent -notcontains $resourceType) { - $resourceTypesThatGeneratedCostCurrencyIndependent += $resourceType - } - } - $script:htManagementGroupsCost.($parentMg).resourceTypesThatGeneratedCostCurrencyIndependent = $resourceTypesThatGeneratedCostCurrencyIndependent - } - } - } - - $totalCost = 0 - $script:tenantSummaryConsumptionDataGrouped = $currency.group | Group-Object -Property ResourceType, ChargeType, MeterCategory - $subsCount = ($tenantSummaryConsumptionDataGrouped.group.subscriptionId | Sort-Object -Unique | Measure-Object).Count - $consumedServiceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceType | Sort-Object -Unique | Measure-Object).Count - $resourceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceId | Sort-Object -Unique | Measure-Object).Count - foreach ($consumptionline in $tenantSummaryConsumptionDataGrouped) { - - $costConsumptionLine = ($consumptionline.group.PreTaxCost | Measure-Object -Sum).Sum - - if ([math]::Round($costConsumptionLine, 2) -eq 0) { - $cost = $costConsumptionLine.ToString('0.0000') - } - else { - $cost = [math]::Round($costConsumptionLine, 2).ToString('0.00') - } - - $null = $script:arrayConsumptionData.Add([PSCustomObject]@{ - ResourceType = ($consumptionline.name).split(', ')[0] - ConsumedServiceChargeType = ($consumptionline.name).split(', ')[1] - ConsumedServiceCategory = ($consumptionline.name).split(', ')[2] - ConsumedServiceInstanceCount = $consumptionline.Count - ConsumedServiceCost = $cost #[decimal]$cost - ConsumedServiceSubscriptions = ($consumptionline.group.SubscriptionId | Sort-Object -Unique).Count - ConsumedServiceCurrency = $currency.Name - }) - - $totalCost = $totalCost + $costConsumptionLine - - } - if ([math]::Round($totalCost, 2) -eq 0) { - $totalCost = $totalCost - } - else { - $totalCost = [math]::Round($totalCost, 2).ToString('0.00') - } - $script:arrayTotalCostSummary += "$($totalCost) $($currency.Name) generated by $($resourceCount) Resources ($($consumedServiceCount) ResourceTypes) in $($subsCount) Subscriptions" - } - } - else { - Write-Host ' No relevant consumption data entries (0)' - } - } - - #region BuildConsumptionCSV - if (-not $NoCsvExport) { - if (-not $NoAzureConsumptionReportExportToCSV) { - Write-Host " Exporting Consumption CSV $($outputPath)$($DirectorySeparatorChar)$($fileName)_Consumption.csv" - $startBuildConsumptionCSV = Get-Date - if ($CsvExportUseQuotesAsNeeded) { - $allConsumptionData | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_Consumption.csv" -Delimiter "$csvDelimiter" -NoTypeInformation -UseQuotes AsNeeded - } - else { - $allConsumptionData | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_Consumption.csv" -Delimiter "$csvDelimiter" -NoTypeInformation - } - $endBuildConsumptionCSV = Get-Date - Write-Host " Exporting Consumption CSV total duration: $((New-TimeSpan -Start $startBuildConsumptionCSV -End $endBuildConsumptionCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildConsumptionCSV -End $endBuildConsumptionCSV).TotalSeconds) seconds)" - } - } - #endregion BuildConsumptionCSV - } - $endConsumptionData = Get-Date - Write-Host "Getting Consumption data duration: $((New-TimeSpan -Start $startConsumptionData -End $endConsumptionData).TotalSeconds) seconds" -} -function getDefaultManagementGroup { - $currentTask = 'Get Default Management Group' - Write-Host $currentTask - #https://learn.microsoft.com/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2020-02-01" - $method = 'GET' - $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - if (($settingsMG).count -gt 0) { - Write-Host " default ManagementGroup Id: $($settingsMG.properties.defaultManagementGroup)" - $script:defaultManagementGroupId = $settingsMG.properties.defaultManagementGroup - Write-Host " requireAuthorizationForGroupCreation: $($settingsMG.properties.requireAuthorizationForGroupCreation)" - $script:requireAuthorizationForGroupCreation = $settingsMG.properties.requireAuthorizationForGroupCreation - } - else { - Write-Host " default ManagementGroup: $(($azAPICallConf['checkContext']).Tenant.Id) (Tenant Root)" - $script:defaultManagementGroupId = ($azAPICallConf['checkContext']).Tenant.Id - $script:requireAuthorizationForGroupCreation = $false - } -} -function getEntities { - Write-Host 'Entities' - $startEntities = Get-Date - $currentTask = ' Getting Entities' - Write-Host $currentTask - #https://management.azure.com/providers/Microsoft.Management/getEntities?api-version=2020-02-01 - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/getEntities?api-version=2020-02-01" - $method = 'POST' - $arrayEntitiesFromAPIInitial = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - Write-Host " $($arrayEntitiesFromAPIInitial.Count) Entities returned" - - $script:arrayEntitiesFromAPI = [System.Collections.ArrayList]@() - $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions = @{} - foreach ($entry in $arrayEntitiesFromAPIInitial) { - if ($entry.Type -eq '/subscriptions') { - if ($htSubscriptionsFromOtherTenants.($entry.name)) { - $subdetail = $htSubscriptionsFromOtherTenants.($entry.name).subdetails - Write-Host " Excluded Subscription '$($subDetail.displayName)' ($($entry.name)) (foreign tenantId: '$($subDetail.tenantId)')" -ForegroundColor DarkRed - continue - } - if (-not $htAllSubscriptionsFromAPI.($entry.name)) { - #not contained in subscriptions - $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($entry.name) = $entry - Write-Host " Excluded Subscription '$($entry.properties.displayName)' ($($entry.name)) (contained in GetEntities, not contained in GetSubscriptions)" -ForegroundColor DarkRed - continue - } - #test - # if ($entry.name -eq '') { - # $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($entry.name) = $entry - # Write-Host " Excluded Subscription '$($entry.properties.displayName)' ($($entry.name)) (contained in GetEntities, not contained in GetSubscriptions)" -ForegroundColor DarkRed - # continue - # } - } - - $null = $script:arrayEntitiesFromAPI.Add($entry) - } - - Write-Host " $($arrayEntitiesFromAPI.Count)/$($arrayEntitiesFromAPIInitial.Count) Entities relevant" - - $endEntities = Get-Date - Write-Host " Getting Entities duration: $((New-TimeSpan -Start $startEntities -End $endEntities).TotalSeconds) seconds" - - $startEntitiesdata = Get-Date - Write-Host ' Processing Entities data' - $script:htSubscriptionsMgPath = @{} - $script:htManagementGroupsMgPath = @{} - $script:htEntities = @{} - $script:htEntitiesPlain = @{} - - foreach ($entity in $arrayEntitiesFromAPI) { - $script:htEntitiesPlain.($entity.Name) = @{} - $script:htEntitiesPlain.($entity.Name) = $entity - } - - foreach ($entity in $arrayEntitiesFromAPI) { - if ($entity.Type -eq '/subscriptions') { - $parent = $entity.properties.parent.Id -replace '.*/' - $parentId = $entity.properties.parent.Id - $script:htSubscriptionsMgPath.($entity.name) = @{} - $script:htSubscriptionsMgPath.($entity.name).ParentNameChain = $entity.properties.parentNameChain - $script:htSubscriptionsMgPath.($entity.name).ParentNameChainDelimited = $entity.properties.parentNameChain -join '/' - $script:htSubscriptionsMgPath.($entity.name).Parent = $entity.properties.parent.Id -replace '.*/' - $script:htSubscriptionsMgPath.($entity.name).ParentName = $htEntitiesPlain.($entity.properties.parent.Id -replace '.*/').properties.displayName - $script:htSubscriptionsMgPath.($entity.name).DisplayName = $entity.properties.displayName - $array = $entity.properties.parentNameChain - $array += $entity.name - $script:htSubscriptionsMgPath.($entity.name).path = $array - $script:htSubscriptionsMgPath.($entity.name).pathDelimited = $array -join '/' - $script:htSubscriptionsMgPath.($entity.name).level = (($entity.properties.parentNameChain).Count - 1) - } - if ($entity.Type -eq 'Microsoft.Management/managementGroups') { - if ([string]::IsNullOrEmpty($entity.properties.parent.Id)) { - $parent = '__TenantRoot__' - $parentId = '__TenantRoot__' - } - else { - $parent = $entity.properties.parent.Id -replace '.*/' - $parentId = $entity.properties.parent.Id - } - $script:htManagementGroupsMgPath.($entity.name) = @{} - $script:htManagementGroupsMgPath.($entity.name).ParentNameChain = $entity.properties.parentNameChain - $script:htManagementGroupsMgPath.($entity.name).ParentNameChainDelimited = $entity.properties.parentNameChain -join '/' - $script:htManagementGroupsMgPath.($entity.name).ParentNameChainCount = ($entity.properties.parentNameChain | Measure-Object).Count - $script:htManagementGroupsMgPath.($entity.name).Parent = $parent - $script:htManagementGroupsMgPath.($entity.name).ChildMgsAll = ($arrayEntitiesFromAPI.where( { $_.Type -eq 'Microsoft.Management/managementGroups' -and $_.properties.ParentNameChain -contains $entity.name } )).Name - $script:htManagementGroupsMgPath.($entity.name).ChildMgsDirect = ($arrayEntitiesFromAPI.where( { $_.Type -eq 'Microsoft.Management/managementGroups' -and $_.properties.Parent.Id -replace '.*/' -eq $entity.name } )).Name - $script:htManagementGroupsMgPath.($entity.name).DisplayName = $entity.properties.displayName - $script:htManagementGroupsMgPath.($entity.name).Id = ($entity.name) - $array = $entity.properties.parentNameChain - $array += $entity.name - $script:htManagementGroupsMgPath.($entity.name).path = $array - $script:htManagementGroupsMgPath.($entity.name).pathDelimited = $array -join '/' - $script:htManagementGroupsMgPath.($entity.name).level = $array.Count - } - - $script:htEntities.($entity.name) = @{} - $script:htEntities.($entity.name).ParentNameChain = $entity.properties.parentNameChain - $script:htEntities.($entity.name).Parent = $parent - $script:htEntities.($entity.name).ParentId = $parentId - if ($parent -eq '__TenantRoot__') { - $parentDisplayName = '__TenantRoot__' - } - else { - $parentDisplayName = $htEntitiesPlain.($htEntities.($entity.name).Parent).properties.displayName - } - $script:htEntities.($entity.name).ParentDisplayName = $parentDisplayName - $script:htEntities.($entity.name).DisplayName = $entity.properties.displayName - $script:htEntities.($entity.name).Id = $entity.Name - $script:htEntities.($entity.name).Type = $entity.Type - } - - Write-Host " $(($htManagementGroupsMgPath.Keys).Count) relevant Management Groups" - Write-Host " $(($htSubscriptionsMgPath.Keys).Count) relevant Subscriptions" - - $endEntitiesdata = Get-Date - Write-Host " Processing Entities data duration: $((New-TimeSpan -Start $startEntitiesdata -End $endEntitiesdata).TotalSeconds) seconds" - - $script:arrayEntitiesFromAPISubscriptionsCount = ($arrayEntitiesFromAPI.where( { $_.type -eq '/subscriptions' -and $_.properties.parentNameChain -contains $ManagementGroupId } ) | Sort-Object -Property id -Unique).count - $script:arrayEntitiesFromAPIManagementGroupsCount = ($arrayEntitiesFromAPI.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.properties.parentNameChain -contains $ManagementGroupId } ) | Sort-Object -Property id -Unique).count + 1 - - $endEntities = Get-Date - Write-Host "Processing Entities duration: $((New-TimeSpan -Start $startEntities -End $endEntities).TotalSeconds) seconds" -} -function getFileNaming { - if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { - if ($HierarchyMapOnly) { - $script:fileName = "AzGovViz_HierarchyMapOnly_$($ManagementGroupId)" - } - elseif ($azAPICallConf['htParameters'].ManagementGroupsOnly -eq $true) { - $script:fileName = "AzGovViz_ManagementGroupsOnly_$($ManagementGroupId)" - } - else { - $script:fileName = "AzGovViz_$($ManagementGroupId)" - } - } - else { - if ($HierarchyMapOnly) { - $script:fileName = "AzGovViz_HierarchyMapOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" - } - elseif ($azAPICallConf['htParameters'].ManagementGroupsOnly -eq $true) { - $script:fileName = "AzGovViz_ManagementGroupsOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" - } - else { - $script:fileName = "AzGovViz_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" - } - } -} - -function getGroupmembers($aadGroupId, $aadGroupDisplayName) { - if (-not $htAADGroupsDetails.($aadGroupId)) { - $script:htAADGroupsDetails.$aadGroupId = @{} - $script:htAADGroupsDetails.($aadGroupId).Id = $aadGroupId - $script:htAADGroupsDetails.($aadGroupId).displayname = $aadGroupDisplayName - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupId)/transitiveMembers" - $method = 'GET' - $aadGroupMembers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupmembers $($aadGroupId)" - - if ($aadGroupMembers -eq 'Request_ResourceNotFound') { - $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ - groupId = $aadGroupId - }) - } - - $aadGroupMembersAll = ($aadGroupMembers) - $aadGroupMembersUsers = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.user' } ) - $aadGroupMembersGroups = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.group' } ) - $aadGroupMembersServicePrincipals = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.servicePrincipal' } ) - - $aadGroupMembersAllCount = $aadGroupMembersAll.count - $aadGroupMembersUsersCount = $aadGroupMembersUsers.count - $aadGroupMembersGroupsCount = $aadGroupMembersGroups.count - $aadGroupMembersServicePrincipalsCount = $aadGroupMembersServicePrincipals.count - #for SP stuff - if ($aadGroupMembersServicePrincipalsCount -gt 0) { - foreach ($identity in $aadGroupMembersServicePrincipals) { - $arrayIdentityObject = [System.Collections.ArrayList]@() - if ($identity.servicePrincipalType -eq 'Application') { - if ($identity.appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Tenant.Id) { - $null = $arrayIdentityObject.Add([PSCustomObject]@{ - type = 'ServicePrincipal' - spTypeConcatinated = 'SP APP INT' - servicePrincipalType = $identity.servicePrincipalType - id = $identity.id - appid = $identity.appId - displayName = $identity.displayName - appOwnerOrganizationId = $identity.appOwnerOrganizationId - alternativeNames = $identity.alternativeNames - }) - } - else { - $null = $arrayIdentityObject.Add([PSCustomObject]@{ - type = 'ServicePrincipal' - spTypeConcatinated = 'SP APP EXT' - servicePrincipalType = $identity.servicePrincipalType - id = $identity.id - appid = $identity.appId - displayName = $identity.displayName - appOwnerOrganizationId = $identity.appOwnerOrganizationId - alternativeNames = $identity.alternativeNames - }) - } - } - elseif ($identity.servicePrincipalType -eq 'ManagedIdentity') { - $miType = 'unknown' - if ($identity.alternativeNames) { - foreach ($altName in $identity.alternativeNames) { - if ($altName -like 'isExplicit=*') { - $splitAltName = $altName.split('=') - if ($splitAltName[1] -eq 'true') { - $miType = 'Usr' - } - if ($splitAltName[1] -eq 'false') { - $miType = 'Sys' - } - } - } - } - $null = $arrayIdentityObject.Add([PSCustomObject]@{ - type = 'ServicePrincipal' - spTypeConcatinated = "SP MI $miType" - servicePrincipalType = $identity.servicePrincipalType - id = $identity.id - appid = $identity.appId - displayName = $identity.displayName - appOwnerOrganizationId = $identity.appOwnerOrganizationId - alternativeNames = $identity.alternativeNames - }) - } - else { - $null = $arrayIdentityObject.Add([PSCustomObject]@{ - type = 'servicePrincipal' - spTypeConcatinated = "SP $($identity.servicePrincipalType)" - servicePrincipalType = $identity.servicePrincipalType - id = $identity.id - appid = $identity.appId - displayName = $identity.displayName - appOwnerOrganizationId = $identity.appOwnerOrganizationId - alternativeNames = $identity.alternativeNames - }) - } - if (-not $htServicePrincipals.($identity.id)) { - #Write-Host "$($identity.displayName) $($identity.id) added - - - - - - - - " - $script:htServicePrincipals.($identity.id) = @{} - $script:htServicePrincipals.($identity.id) = $arrayIdentityObject - } - } - } - - #guests - if ($aadGroupMembersUsersCount -gt 0) { - $cntx = 0 - $cnty = 0 - foreach ($aadGroupMembersUser in $aadGroupMembersUsers | Sort-Object -Property id -Unique) { - $cntx++ - if ($aadGroupMembersUser.userType -eq 'Guest') { - if (-not $htUserTypesGuest.($aadGroupMembersUser.id)) { - $cnty++ - #Write-Host "$($aadGroupMembersUser.id) is Guest" - $script:htUserTypesGuest.($aadGroupMembersUser.id) = @{} - $script:htUserTypesGuest.($aadGroupMembersUser.id).userType = 'Guest' - } - else { - #Write-Host "$($aadGroupMembersUser.id) already known as Guest" - } - } - } - } - - $script:htAADGroupsDetails.($aadGroupId).MembersAllCount = $aadGroupMembersAllCount - $script:htAADGroupsDetails.($aadGroupId).MembersUsersCount = $aadGroupMembersUsersCount - $script:htAADGroupsDetails.($aadGroupId).MembersGroupsCount = $aadGroupMembersGroupsCount - $script:htAADGroupsDetails.($aadGroupId).MembersServicePrincipalsCount = $aadGroupMembersServicePrincipalsCount - - if ($aadGroupMembersAllCount -gt 0) { - $script:htAADGroupsDetails.($aadGroupId).MembersAll = $aadGroupMembersAll - - if ($aadGroupMembersUsersCount -gt 0) { - $script:htAADGroupsDetails.($aadGroupId).MembersUsers = $aadGroupMembersUsers - } - if ($aadGroupMembersGroupsCount -gt 0) { - $script:htAADGroupsDetails.($aadGroupId).MembersGroups = $aadGroupMembersGroups - } - if ($aadGroupMembersServicePrincipalsCount -gt 0) { - $script:htAADGroupsDetails.($aadGroupId).MembersServicePrincipals = $aadGroupMembersServicePrincipals - } - } - } -} -function getMDfCSecureScoreMG { - $start = Get-Date - $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' - Write-Host $currentTask - #ref: https://learn.microsoft.com/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" - $method = 'POST' - - $query = @' - SecurityResources - | where type == 'microsoft.security/securescores' - | project subscriptionId, - subscriptionTotal = iff(properties.score.max == 0, 0.00, round(tolong(properties.weight) * todouble(properties.score.current)/tolong(properties.score.max),2)), - weight = tolong(iff(properties.weight == 0, 1, properties.weight)) - | join kind=leftouter ( - ResourceContainers - | where type == 'microsoft.resources/subscriptions' and properties.state == 'Enabled' - | project subscriptionId, mgChain=properties.managementGroupAncestorsChain ) - on subscriptionId - | mv-expand mg=mgChain - | summarize sumSubs = sum(subscriptionTotal), sumWeight = sum(weight), resultsNum = count() by tostring(mg.displayName), mgId = tostring(mg.name) - | extend secureScore = iff(tolong(resultsNum) == 0, 404.00, round(sumSubs/sumWeight*100,2)) - | project mgDisplayName=mg_displayName, mgId, sumSubs, sumWeight, resultsNum, secureScore - | order by mgDisplayName asc -'@ - - $body = @" - { - "query": "$($query)", - "managementGroups":[ - "$($ManagementGroupId)" - ] - } -"@ - - $getMgAscSecureScore = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' - - if ($getMgAscSecureScore) { - if ($getMgAscSecureScore -eq 'capitulation') { - Write-Host ' Microsoft Defender for Cloud SecureScore for Management Groups will not be available' -ForegroundColor Yellow - } - else { - Write-Host " Retrieved 'Microsoft Defender for Cloud' SecureScore for $($getMgAscSecureScore.Count) Management Groups" - foreach ($entry in $getMgAscSecureScore) { - $script:htMgASCSecureScore.($entry.mgId) = @{} - if ($entry.secureScore -eq 404) { - $script:htMgASCSecureScore.($entry.mgId).SecureScore = 'n/a' - } - else { - $script:htMgASCSecureScore.($entry.mgId).SecureScore = $entry.secureScore - } - } - } - } - - $end = Get-Date - Write-Host "Getting Microsoft Defender for Cloud Secure Score for Management Groups duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" -} -function getOrphanedResources { - $start = Get-Date - Write-Host 'Getting orphaned/unused resources (ARG)' - - $queries = [System.Collections.ArrayList]@() - $intent = 'cost savings - stopped but not deallocated VM' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.compute/virtualmachines' - query = "Resources | where type =~ 'microsoft.compute/virtualmachines' and properties.extended.instanceView.powerState.code =~ 'PowerState/stopped' | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'clean up' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.resources/subscriptions/resourceGroups' - query = "ResourceContainers | where type =~ 'microsoft.resources/subscriptions/resourceGroups' | extend rgAndSub = strcat(resourceGroup, '--', subscriptionId) | join kind=leftouter (Resources | extend rgAndSub = strcat(resourceGroup, '--', subscriptionId) | summarize count() by rgAndSub) on rgAndSub | where isnull(count_) | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'misconfiguration' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.network/networkSecurityGroups' - query = "Resources | where type =~ 'microsoft.network/networkSecurityGroups' and isnull(properties.networkInterfaces) and isnull(properties.subnets) | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'misconfiguration' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.network/routeTables' - query = "resources | where type =~ 'microsoft.network/routeTables' | where isnull(properties.subnets) | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'misconfiguration' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.network/networkInterfaces' - query = "Resources | where type =~ 'microsoft.network/networkInterfaces' | where isnull(properties.privateEndpoint) | where isnull(properties.privateLinkService) | where properties.hostedWorkloads == '[]' | where properties !has 'virtualmachine' | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'cost savings' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.compute/disks' - query = "Resources | where type =~ 'microsoft.compute/disks' | extend diskState = tostring(properties.diskState) | where managedBy == '' | where not(name endswith '-ASRReplica' or name startswith 'ms-asr-' or name startswith 'asrseeddisk-') | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'cost savings' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.network/publicIpAddresses' - query = "Resources | where type =~ 'microsoft.network/publicIpAddresses' | where properties.ipConfiguration == '' and properties.natGateway == '' | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'misconfiguration' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.compute/availabilitySets' - query = "Resources | where type =~ 'microsoft.compute/availabilitySets' | where properties.virtualMachines == '[]' | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'misconfiguration' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.network/loadBalancers' - query = "Resources | where type =~ 'microsoft.network/loadBalancers' | where properties.backendAddressPools == '[]' | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $intent = 'cost savings' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.network/applicationGateways' - query = "resources | where type =~ 'Microsoft.Network/applicationGateways' | extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools | project type, subscriptionId, Resource=id, Intent='$intent' | join (resources | where type =~ 'Microsoft.Network/applicationGateways' | mvexpand backendPools = properties.backendAddressPools | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations) | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) | extend backendPoolName = backendPools.properties.backendAddressPools.name | extend Resource = id | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by Resource) on Resource | project-away Resource1 | where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount)) | order by Resource asc" - intent = $intent - }) - - $intent = 'cost savings' - $null = $queries.Add([PSCustomObject]@{ - queryName = 'microsoft.web/serverfarms' - query = "Resources | where type =~ 'microsoft.web/serverfarms' | where properties.numberOfSites == 0 | project type, subscriptionId, Resource=id, Intent='$intent'" - intent = $intent - }) - - $queries | ForEach-Object -Parallel { - $queryDetail = $_ - $arrayOrphanedResources = $using:arrayOrphanedResources - $subsToProcessInCustomDataCollection = $using:subsToProcessInCustomDataCollection - $azAPICallConf = $using:azAPICallConf - - #Batching: https://learn.microsoft.com/azure/governance/resource-graph/troubleshoot/general#toomanysubscription - $counterBatch = [PSCustomObject] @{ Value = 0 } - $batchSize = 1000 - $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } - - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" - $method = 'POST' - foreach ($batch in $subscriptionsBatch) { - Write-Host " Getting orphaned $($queryDetail.queryName) for $($batch.Group.subscriptionId.Count) Subscriptions" - $subscriptions = '"{0}"' -f ($batch.Group.subscriptionId -join '","') - $body = @" -{ - "query": "$($queryDetail.query)", - "subscriptions": [$($subscriptions)], - "options": { - "`$top": 1000 - } -} -"@ - - $res = (AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -listenOn 'Content' -currentTask "Getting orphaned $($queryDetail.queryName)") - #Write-Host '$res.count:' $res.count - if ($res.count -gt 0) { - foreach ($resource in $res) { - $null = $script:arrayOrphanedResources.Add($resource) - } - } - Write-Host " $($res.count) orphaned $($queryDetail.queryName) found" - } - } -ThrottleLimit ($queries.Count) - - if ($arrayOrphanedResources.Count -gt 0) { - - if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { - $allConsumptionDataGroupedByTypeAndCurrency = $allConsumptionData | Group-Object -Property ResourceType, Currency - $orphanedResourcesResourceTypesCostRelevant = ($queries.where({ $_.intent -like 'cost savings*' })).queryName - - $htC = @{} - foreach ($consumptionResourceTypeAndCurrency in $allConsumptionDataGroupedByTypeAndCurrency) { - $consumptionResourceTypeAndCurrencySplitted = $consumptionResourceTypeAndCurrency.Name.split(', ') - if ($consumptionResourceTypeAndCurrencySplitted[0] -in $orphanedResourcesResourceTypesCostRelevant ) { - foreach ($entry in $consumptionResourceTypeAndCurrency.Group) { - if (-not $htC.($entry.resourceId)) { - $htC.($entry.resourceId) = @{} - $htC.($entry.resourceId).cost = $entry.PreTaxCost - $htC.($entry.resourceId).currency = $entry.Currency - } - else { - $htC.($entry.resourceId).cost = $htC.($entry.resourceId).cost + $entry.PreTaxCost - } - } - } - } - - $costrelevantOrphanedResourcesGroupedByType = ($arrayOrphanedResources | Group-Object -Property intent).where({ $_.name -like 'cost savings*' }).group | Group-Object -Property type - $nonCostrelevantOrphanedResourcesGroupedByType = ($arrayOrphanedResources | Group-Object -Property intent).where({ $_.name -notlike 'cost savings*' }).group | Group-Object -Property type - $script:arrayOrphanedResources = [System.Collections.ArrayList]@() - - foreach ($costrelevantOrphanedResourceType in $costrelevantOrphanedResourcesGroupedByType) { - foreach ($resource in $costrelevantOrphanedResourceType.Group) { - if ($htC.($resource.Resource)) { - $null = $script:arrayOrphanedResources.Add([PSCustomObject]@{ - Type = $costrelevantOrphanedResourceType.Name - Resource = $resource.Resource - SubscriptionId = $resource.subscriptionId - Intent = $resource.Intent - Cost = $htC.($resource.Resource).cost - Currency = $htC.($resource.Resource).currency - }) - } - else { - $null = $script:arrayOrphanedResources.Add([PSCustomObject]@{ - Type = $costrelevantOrphanedResourceType.Name - Resource = $resource.Resource - SubscriptionId = $resource.subscriptionId - Intent = $resource.Intent - Cost = '' - Currency = '' - }) - } - } - } - - foreach ($nonCostrelevantOrphanedResourceType in $nonCostrelevantOrphanedResourcesGroupedByType) { - Write-Host "Processing $($nonCostrelevantOrphanedResourceType.Name)" - foreach ($resource in $nonCostrelevantOrphanedResourceType.Group) { - $null = $script:arrayOrphanedResources.Add([PSCustomObject]@{ - Type = $nonCostrelevantOrphanedResourceType.Name - Resource = $resource.Resource - SubscriptionId = $resource.subscriptionId - Intent = $resource.Intent - Cost = '' - Currency = '' - }) - } - } - } - - Write-Host " Found $($arrayOrphanedResources.Count) orphaned/unused Resources" - if (-not $NoCsvExport) { - Write-Host " Exporting OrphanedResources CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesCostOptimizationAndCleanup.csv'" - $arrayOrphanedResources | Sort-Object -Property Resource | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesCostOptimizationAndCleanup.csv" -Delimiter "$csvDelimiter" -NoTypeInformation - } - } - else { - Write-Host ' No orphaned/unused Resources found' - } - - $end = Get-Date - Write-Host "Getting orphaned/unused resources (ARG) processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" -} -function getPIMEligible { - $start = Get-Date - - $currentTask = 'Get PIM onboarded Subscriptions and Management Groups' - Write-Host $currentTask - $uriExt = "&`$expand=parent&`$filter=(type eq 'subscription' or type eq 'managementgroup')" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/resources?`$select=id,displayName,type,externalId" + $uriExt - $res = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -currentTask $currentTask - if ($res.Count -gt 0) { - - $scopesToIterate = [System.Collections.ArrayList]@() - if (-not $PIMEligibilityIgnoreScope) { - if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { - foreach ($entry in $res) { - if ($entry.type -eq 'managementGroup') { - if ($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain -contains ($entry.externalId -replace '.*/') -or $htManagementGroupsMgPath.($entry.externalId -replace '.*/').path -contains $ManagementGroupId) { - $null = $scopesToIterate.Add($entry) - } - } - if ($entry.type -eq 'subscription') { - if ($htSubscriptionsMgPath.($entry.externalId -replace '.*/').ParentNameChain -contains $ManagementGroupId) { - if ($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/')) { - Write-Host "excluding subscription $($entry.externalId -replace '.*/') (outOfScopeSubscription -> $($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/').outOfScopeReason)) (`$PIMEligibilityIgnoreScope=$PIMEligibilityIgnoreScope)" - } - else { - $null = $scopesToIterate.Add($entry) - } - } - } - } - } - else { - foreach ($entry in $res) { - if ($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/')) { - Write-Host "excluding subscription $($entry.externalId -replace '.*/') (outOfScopeSubscription -> $($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/').outOfScopeReason)) (`$PIMEligibilityIgnoreScope=$PIMEligibilityIgnoreScope)" - } - else { - $null = $scopesToIterate.Add($entry) - } - } - } - } - else { - foreach ($entry in $res) { - $null = $scopesToIterate.Add($entry) - } - } - - $PIMOnboardedGrouped = $scopesToIterate | Group-Object -Property type - foreach ($entry in $PIMOnboardedGrouped) { - Write-Host " Found $($entry.Count) PIM onboarded $($entry.Name)s" - } - - $htPIMEligibleDirect = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} - $relevantSubscriptionIds = $subsToProcessInCustomDataCollection.subscriptionId - $scopesToIterate | ForEach-Object -Parallel { - $scope = $_ - $azAPICallConf = $using:azAPICallConf - $arrayPIMEligible = $using:arrayPIMEligible - $htPIMEligibleDirect = $using:htPIMEligibleDirect - if ($scope.type -eq 'managementgroup') { $htManagementGroupsMgPath = $using:htManagementGroupsMgPath } - if ($scope.type -eq 'subscription') { $htSubscriptionsMgPath = $using:htSubscriptionsMgPath } - $htPrincipals = $using:htPrincipals - $htUserTypesGuest = $using:htUserTypesGuest - $htServicePrincipals = $using:htServicePrincipals - $relevantSubscriptionIds = $using:relevantSubscriptionIds - $function:resolveObjectIds = $using:funcResolveObjectIds - $function:testGuid = $using:funcTestGuid - - $processThisScope = $true - if ($scope.type -eq 'subscription') { - if (($scope.externalId -replace '.*/') -notin $relevantSubscriptionIds) { - Write-Host " Non relevant subscriptionId '$(($scope.externalId -replace '.*/'))' /skipping this subscription as it is not contained in the 'Relevant Subscriptions' collection (needs investigation)" -ForegroundColor DarkRed - $processThisScope = $false - } - } - - if ($processThisScope -eq $true) { - $currentTask = "Get Eligible assignments for Scope $($scope.type): $($scope.externalId -replace '.*/')" - $extUri = "?`$expand=linkedEligibleRoleAssignment,subject,roleDefinition(`$expand=resource)&`$count=true&`$filter=(roleDefinition/resource/id eq '$($scope.id)')+and+(assignmentState eq 'Eligible')&`$top=100" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/roleAssignments" + $extUri - $resx = AzAPICall -AzAPICallConfiguration $azapicallConf -currentTask $currentTask -uri $uri - - if ($resx.Count -gt 0) { - - $users = $resx.where({ $_.subject.type -eq 'user' }) - if ($users.Count -gt 0) { - ResolveObjectIds -objectIds $users.subject.id -showActivity - } - - foreach ($entry in $resx) { - $scopeId = $scope.externalId -replace '.*/' - if ($scope.type -eq 'managementgroup') { - $ScopeType = 'MG' - $ManagementGroupId = $scopeId - $SubscriptionId = '' - $SubscriptionDisplayName = '' - if ($htManagementGroupsMgPath.($scopeId)) { - $MgDetails = $htManagementGroupsMgPath.($scopeId) - $ManagementGroupDisplayName = $MgDetails.DisplayName - $ScopeDisplayName = $MgDetails.DisplayName - $MgPath = $MgDetails.path - $MgLevel = $MgDetails.level - } - else { - $ManagementGroupDisplayName = 'notAccessible' - $ScopeDisplayName = 'notAccessible' - $MgPath = 'notAccessible' - $MgLevel = 'notAccessible' - } - - if ($entry.memberType -eq 'direct') { - $script:htPIMEligibleDirect.($entry.id) = @{} - $script:htPIMEligibleDirect.($entry.id).clear = $scopeId - if ($scopeId -eq $ManagementGroupDisplayName) { - $script:htPIMEligibleDirect.($entry.id).enriched = "$($scopeId) [Level $($MgLevel)]" - } - else { - $script:htPIMEligibleDirect.($entry.id).enriched = "$($ManagementGroupDisplayName) ($($scopeId)) [Level $($MgLevel)]" - } - } - } - if ($scope.type -eq 'subscription') { - $ScopeType = 'Sub' - #$ManagementGroupId = '' - $SubscriptionId = $scopeId - if ($htSubscriptionsMgPath.($scopeId)) { - $MgDetails = $htSubscriptionsMgPath.($scopeId) - $SubscriptionDisplayName = $MgDetails.DisplayName - $ScopeDisplayName = $MgDetails.DisplayName - $MgPath = $MgDetails.path - $MgLevel = $MgDetails.level - $ManagementGroupId = $MgDetails.Parent - $ManagementGroupDisplayName = $MgDetails.ParentName - } - else { - $SubscriptionDisplayName = 'notAccessible' - $ScopeDisplayName = 'notAccessible' - $MgPath = 'notAccessible' - $MgLevel = 'notAccessible' - } - #$ManagementGroupDisplayName = '' - - } - - if ($entry.subject.type -eq 'user') { - if ($htPrincipals.($entry.subject.id)) { - $userDetail = $htPrincipals.($entry.subject.id) - $principalType = "$($userDetail.type) $($userDetail.userType)" - } - else { - $principalType = $entry.subject.type - } - } - else { - $principalType = $entry.subject.type - } - - $roleType = 'undefined' - if ($entry.roleDefinition.type -eq 'BuiltInRole') { $roleType = 'Builtin' } - if ($entry.roleDefinition.type -eq 'CustomRole') { $roleType = 'Custom' } - - $null = $script:arrayPIMEligible.Add([PSCustomObject]@{ - ScopeType = $ScopeType - ScopeId = $scopeId - ScopeDisplayName = $ScopeDisplayName - ManagementGroupId = $ManagementGroupId - ManagementGroupDisplayName = $ManagementGroupDisplayName - SubscriptionId = $SubscriptionId - SubscriptionDisplayName = $SubscriptionDisplayName - MgPath = $MgPath - MgLevel = $MgLevel - RoleId = $entry.roleDefinition.externalId - RoleIdGuid = $entry.roleDefinition.externalId -replace '.*/' - RoleType = $roleType - RoleName = $entry.roleDefinition.displayName - IdentityObjectId = $entry.subject.id - IdentityType = $principalType - IdentityDisplayName = $entry.subject.displayName - IdentityPrincipalName = $entry.subject.principalName - PIMId = $entry.id - PIMInheritance = $entry.memberType - PIMInheritedFromClear = '' - PIMInheritedFrom = '' - PIMStartDateTime = $entry.startDateTime - PIMEndDateTime = $entry.endDateTime - }) - } - } - } - - } -ThrottleLimit $ThrottleLimit - - foreach ($entry in $arrayPIMEligible) { - if ($entry.PIMInheritance -eq 'inherited') { - $entry.PIMInheritedFromClear = $htPIMEligibleDirect.($entry.PIMId).clear - $entry.PIMInheritedFrom = $htPIMEligibleDirect.($entry.PIMId).enriched - } - } - - $script:arrayPIMEligibleGrouped = $arrayPIMEligible | Group-Object -Property ScopeType - foreach ($entry in $arrayPIMEligibleGrouped) { - Write-Host " Found $($entry.Count) PIM Eligible assignments for $($entry.Name)s" - } - } - - $end = Get-Date - Write-Host "Getting PIM Eligible assignments processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" -} -function getPolicyHash { - [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] - $json - ) - return [System.BitConverter]::ToString([System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($json))) -} -function getPolicyRemediation { - $currentTask = 'Getting NonCompliant (dine/modify)' - Write-Host $currentTask - #ref: https://learn.microsoft.com/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" - $method = 'POST' - - if ($ManagementGroupsOnly) { - $queryNonCompliant = @' - policyresources - | where type == 'microsoft.policyinsights/policystates' and properties.policyAssignmentScope startswith '/providers/Microsoft.Management/managementGroups/' and (properties.policyDefinitionAction =~ 'deployifnotexists' or properties.policyDefinitionAction =~ 'modify') and properties.complianceState =~ 'NonCompliant' - | summarize count() by assignmentScope = tostring(properties.policyAssignmentScope), assignmentName = tostring(properties.policyAssignmentName), assignmentId = tostring(properties.policyAssignmentId), definitionName = tostring(properties.policyDefinitionName), definitionId = tostring(properties.policyDefinitionId), policyDefinitionReferenceId = tostring(properties.policyDefinitionReferenceId), effect = tostring(properties.policyDefinitionAction) - | sort by count_, assignmentId, definitionId, policyDefinitionReferenceId, effect -'@ - } - else { - $queryNonCompliant = @' - policyresources - | where (properties.policyDefinitionAction =~ 'deployifnotexists' or properties.policyDefinitionAction =~ 'modify') and properties.complianceState =~ 'NonCompliant' - | summarize count() by assignmentScope = tostring(properties.policyAssignmentScope), assignmentName = tostring(properties.policyAssignmentName), assignmentId = tostring(properties.policyAssignmentId), definitionName = tostring(properties.policyDefinitionName), definitionId = tostring(properties.policyDefinitionId), policyDefinitionReferenceId = tostring(properties.policyDefinitionReferenceId), effect = tostring(properties.policyDefinitionAction) - | sort by count_, assignmentId, definitionId, policyDefinitionReferenceId, effect -'@ - } - - - $body = @" - { - "query": "$($queryNonCompliant)", - "managementGroups":[ - "$($ManagementGroupId)" - ], - "options": { - "`$top": 1000 - } - } -"@ - - $getNonCompliant = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' - $script:arrayRemediatable = [System.Collections.ArrayList]@() - Write-Host " Found $($getNonCompliant.Count) remediatable Policy definitions" - if ($getNonCompliant.Count -gt 0) { - Write-Host ' Enriching remediatable assignments with displayNames' - foreach ($nonCompliant in $getNonCompliant) { - - if ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower())) { - if ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { - $policyAssignmentPolicyOrPolicySet = 'policySetDefinition' - $policySetDefinitionId = $htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId - $policySetDefinitionDisplayName = $htCacheDefinitionsPolicySet.($policySetDefinitionId.ToLower()).DisplayName - $policySetDefinitionName = $policySetDefinitionId -replace '.*/' - $policySetDefinitionType = $htCacheDefinitionsPolicySet.($policySetDefinitionId.ToLower()).Type - } - elseif ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { - $policyAssignmentPolicyOrPolicySet = 'policyDefinition' - $policySetDefinitionId = 'n/a' - $policySetDefinitionDisplayName = 'n/a' - $policySetDefinitionName = 'n/a' - $policySetDefinitionType = 'n/a' - } - else { - throw "unexpected .policyDefinitionId: $($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties)" - } - - switch ($nonCompliant.assignmentId) { - { $_ -like '/subscriptions/*' } { - $policyAssignmentScopeType = 'Sub' - } - { $_ -like '/subscriptions/*/resourcegroups/*' } { - $policyAssignmentScopeType = 'RG' - } - { $_ -like '/providers/Microsoft.Management/managementGroups/*' } { - $policyAssignmentScopeType = 'MG' - } - default { - $policyAssignmentScopeType = 'notDetected' - } - } - - $null = $script:arrayRemediatable.Add([PSCustomObject]@{ - policyAssignmentScopeType = $policyAssignmentScopeType - policyAssignmentScope = $nonCompliant.assignmentScope - policyAssignmentId = $nonCompliant.assignmentId - policyAssignmentName = $nonCompliant.assignmentName - policyAssignmentDisplayName = $htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.displayName - policyAssignmentPolicyOrPolicySet = $policyAssignmentPolicyOrPolicySet - effect = $nonCompliant.effect - policyDefinitionId = $nonCompliant.definitionId - policyDefinitionName = $nonCompliant.definitionName - policyDefinitionDisplayName = $htCacheDefinitionsPolicy.($nonCompliant.definitionId.toLower()).Json.properties.displayName - policyDefinitionType = $htCacheDefinitionsPolicy.($nonCompliant.definitionId.toLower()).Type - policySetPolicyDefinitionReferenceId = $nonCompliant.policyDefinitionReferenceId - policySetDefinitionId = $policySetDefinitionId - policySetDefinitionName = $policySetDefinitionName - policySetDefinitionDisplayName = $policySetDefinitionDisplayName - policySetDefinitionType = $policySetDefinitionType - nonCompliantResourcesCount = $nonCompliant.count_ - }) - } - else { - Write-Host " skipping `$htCacheAssignmentsPolicy.($($nonCompliant.assignmentId)) potentially an assignment on an out-of-scope subscription" - } - } - } -} -function getResourceDiagnosticsCapability { - Write-Host 'Checking Resource Types Diagnostics capability (1st party only)' - $startResourceDiagnosticsCheck = Get-Date - if (($resourcesAll).count -gt 0) { - - $startGroupResourceIdsByType = Get-Date - $script:resourceTypesUnique = ($resourcesIdsAll | Group-Object -Property type) - $endGroupResourceIdsByType = Get-Date - Write-Host " GroupResourceIdsByType processing duration: $((New-TimeSpan -Start $startGroupResourceIdsByType -End $endGroupResourceIdsByType).TotalSeconds) seconds)" - $resourceTypesUniqueCount = ($resourceTypesUnique | Measure-Object).count - Write-Host " $($resourceTypesUniqueCount) unique Resource Types" - $script:resourceTypesSummarizedArray = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) - - $script:resourceTypesDiagnosticsArray = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) - $microsoftResourceTypes = $resourceTypesUnique.where({ $_.Name.StartsWith('microsoft') }) - if ($microsoftResourceTypes.Count -gt 0) { - $microsoftResourceTypes | ForEach-Object -Parallel { - $resourceTypesUniqueGroup = $_ - $resourcetype = $resourceTypesUniqueGroup.Name - #region UsingVARs - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $ExcludedResourceTypesDiagnosticsCapable = $using:ExcludedResourceTypesDiagnosticsCapable - $resourceTypesDiagnosticsArray = $using:resourceTypesDiagnosticsArray - $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource - $resourceTypesSummarizedArray = $using:resourceTypesSummarizedArray - #endregion UsingVARs - - $skipThisResourceType = $false - if (($ExcludedResourceTypesDiagnosticsCapable).Count -gt 0) { - foreach ($excludedResourceType in $ExcludedResourceTypesDiagnosticsCapable) { - if ($excludedResourceType -eq $resourcetype) { - $skipThisResourceType = $true - } - } - } - - if ($skipThisResourceType -eq $false) { - $resourceCount = $resourceTypesUniqueGroup.Count - - #thx @Jim Britt (Microsoft) https://github.com/JimGBritt/AzurePolicy/tree/master/AzureMonitor/Scripts Create-AzDiagPolicy.ps1 - $responseJSON = '' - $logCategories = @() - $metrics = $false - $logs = $false - - $resourceAvailability = ($resourceCount - 1) - $counterTryForResourceType = 0 - do { - $counterTryForResourceType++ - if ($resourceCount -gt 1) { - $resourceId = $resourceTypesUniqueGroup.Group.Id[$resourceAvailability] - } - else { - $resourceId = $resourceTypesUniqueGroup.Group.Id - } - - $resourceAvailability = $resourceAvailability - 1 - if ($resourceId -like '*+*') { - Write-Host "resourceId '$resourceId' contains bad character '+'; skipping resourceId" - $responseJSON = 'skipResource' - } - else { - $currentTask = "Checking if ResourceType '$resourceType' is capable for Resource Diagnostics using $counterTryForResourceType ResourceId: '$($resourceId)'" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/$($resourceId)/providers/microsoft.insights/diagnosticSettingsCategories?api-version=2021-05-01-preview" - $method = 'GET' - $responseJSON = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri ([uri]::EscapeUriString($uri)) -method $method -currentTask $currentTask - } - - if ($responseJSON -ne 'skipResource') { - if ($responseJSON -eq 'ResourceTypeOrResourceProviderNotSupported') { - Write-Host " ResourceTypeOrResourceProviderNotSupported | The resource type '$($resourcetype)' does not support diagnostic settings." - - } - else { - Write-Host " ResourceTypeSupported | The resource type '$($resourcetype)' supports diagnostic settings." - } - } - else { - Write-Host "resId '$resourceId' skipped" - } - } - until ($resourceAvailability -lt 0 -or $responseJSON -ne 'skipResource') - - if ($resourceAvailability -lt 0 -and $responseJSON -eq 'skipResource') { - Write-Host "tried for all available resourceIds ($($resourceCount)) for resourceType $resourceType, but seems all resourceIds needed to be skipped" - $null = $script:resourceTypesDiagnosticsArray.Add([PSCustomObject]@{ - ResourceType = $resourcetype - Metrics = "n/a - $responseJSON" - Logs = "n/a - $responseJSON" - LogCategories = 'n/a' - ResourceCount = $resourceCount - }) - } - else { - if ($responseJSON) { - foreach ($response in $responseJSON) { - if ($response.properties.categoryType -eq 'Metrics') { - $metrics = $true - } - if ($response.properties.categoryType -eq 'Logs') { - $logs = $true - $logCategories += $response.name - } - } - } - - $null = $script:resourceTypesDiagnosticsArray.Add([PSCustomObject]@{ - ResourceType = $resourcetype - Metrics = $metrics - Logs = $logs - LogCategories = $logCategories - ResourceCount = $resourceCount - }) - } - } - else { - Write-Host "Skipping ResourceType $($resourcetype) as per parameter '-ExcludedResourceTypesDiagnosticsCapable'" - } - } -ThrottleLimit $ThrottleLimit - } - else { - Write-Host ' No 1st party Resource Types at all' - } - - } - else { - Write-Host ' No Resources at all' - } - $endResourceDiagnosticsCheck = Get-Date - Write-Host "Checking Resource Types Diagnostics capability duration: $((New-TimeSpan -Start $startResourceDiagnosticsCheck -End $endResourceDiagnosticsCheck).TotalMinutes) minutes ($((New-TimeSpan -Start $startResourceDiagnosticsCheck -End $endResourceDiagnosticsCheck).TotalSeconds) seconds)" -} -function getSubscriptions { - $startGetSubscriptions = Get-Date - $currentTask = 'Getting all Subscriptions' - Write-Host "$currentTask" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions?api-version=2020-01-01" - $method = 'GET' - $requestAllSubscriptionsAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - $script:htAllSubscriptionsFromAPI = @{} - $script:htSubscriptionsFromOtherTenants = @{} - - Write-Host " $($requestAllSubscriptionsAPI.Count) Subscriptions returned" - foreach ($subscription in $requestAllSubscriptionsAPI) { - - if ($subscription.tenantId -ne $azAPICallConf['checkcontext'].tenant.id) { - Write-Host " Finding: $($subscription.displayName) ($($subscription.subscriptionId)) belongs to foreign tenant '$($subscription.tenantId)' - Azure Governance Visualizer: excluding this Subscripion" -ForegroundColor DarkRed - $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId) = @{} - $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId).subDetails = $subscription - } - else { - $script:htAllSubscriptionsFromAPI.($subscription.subscriptionId) = @{} - $script:htAllSubscriptionsFromAPI.($subscription.subscriptionId).subDetails = $subscription - } - } - Write-Host " $($htAllSubscriptionsFromAPI.Keys.Count) Subscriptions relevant" - - $endGetSubscriptions = Get-Date - Write-Host "Getting all Subscriptions duration: $((New-TimeSpan -Start $startGetSubscriptions -End $endGetSubscriptions).TotalSeconds) seconds" -} -function getTenantDetails { - $currentTask = 'Get Tenant details' - Write-Host $currentTask - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/tenants?api-version=2020-01-01" - $method = 'GET' - $tenantDetailsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - if (($tenantDetailsResult).count -gt 0) { - $tenantDetails = $tenantDetailsResult | Where-Object { $_.tenantId -eq ($azAPICallConf['checkContext']).Tenant.Id } - if ($tenantDetails.displayName) { - $script:tenantDisplayName = $tenantDetails.displayName - Write-Host " Tenant DisplayName: $tenantDisplayName" - } - else { - Write-Host ' Tenant DisplayName: could not be retrieved' - } - - if ($tenantDetails.defaultDomain) { - $script:tenantDefaultDomain = $tenantDetails.defaultDomain - } - } - else { - Write-Host ' something unexpected' - } -} -function handleCloudEnvironment { - Write-Host "Environment: $($azAPICallConf['checkContext'].Environment.Name)" - if ($DoAzureConsumption) { - if ($azAPICallConf['checkContext'].Environment.Name -eq 'AzureChinaCloud') { - Write-Host 'Azure Billing not supported in AzureChinaCloud, skipping Consumption..' - $script:DoAzureConsumption = $false - } - } -} -function NamingValidation($toCheck) { - $checks = @(':', '/', '\', '<', '>', '|', '"') - $array = @() - foreach ($check in $checks) { - if ($toCheck -like "*$($check)*") { - $array += $check - } - } - if ($toCheck -match '\*') { - $array += '*' - } - if ($toCheck -match '\?') { - $array += '?' - } - return $array -} -function prepareData { - Write-Host 'Preparing Data' - $startPreparingArrays = Get-Date - $script:optimizedTableForPathQuery = ($newTable | Select-Object -Property level, mg*, subscription*) | Sort-Object -Property level, mgid, subscriptionId -Unique - $hlperOptimizedTableForPathQuery = $optimizedTableForPathQuery.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) } ) - $script:optimizedTableForPathQueryMgAndSub = ($hlperOptimizedTableForPathQuery | Select-Object -Property level, mg*, subscription*) | Sort-Object -Property level, mgid, mgname, mgparentId, mgparentName, subscriptionId, subscription -Unique - $script:optimizedTableForPathQueryMg = ($optimizedTableForPathQuery.where( { [String]::IsNullOrEmpty($_.SubscriptionId) } ) | Select-Object -Property level, mgid, mgName, mgparentid, mgparentName) | Sort-Object -Property level, mgid, mgname, mgparentId, mgparentName -Unique - $script:optimizedTableForPathQuerySub = ($hlperOptimizedTableForPathQuery | Select-Object -Property subscription*) | Sort-Object -Property subscriptionId -Unique - - foreach ($entry in $optimizedTableForPathQuery) { - $script:htMgDetails.($entry.mgId) = @{} - $mgSubs = $optimizedTableForPathQueryMgAndSub.where( { $_.mgId -eq $entry.mgId } ) - $script:htMgDetails.($entry.mgId).subscriptionsCount = $mgSubs.Count - $script:htMgDetails.($entry.mgId).subscriptions = $mgSubs - $script:htMgDetails.($entry.mgId).details = $entry - $mgChildren = ($optimizedTableForPathQueryMg.where( { $_.mgParentId -eq $entry.mgId } )).MgId - $script:htMgDetails.($entry.mgId).mgChildren = $mgChildren - $script:htMgDetails.($entry.mgId).mgChildrenCount = $mgChildren.Count - } - - foreach ($entry in $optimizedTableForPathQueryMgAndSub) { - $script:htSubDetails.($entry.SubscriptionId) = @{} - $script:htSubDetails.($entry.SubscriptionId).details = $optimizedTableForPathQueryMgAndSub.where( { $_.SubscriptionId -eq $entry.SubscriptionId } ) - } - - $script:parentMgBaseQuery = ($optimizedTableForPathQueryMg.where( { $_.MgParentId -eq $getMgParentId } )) - $script:parentMgNamex = $parentMgBaseQuery.mgParentName | Get-Unique - $script:parentMgIdx = $parentMgBaseQuery.mgParentId | Get-Unique - - $endPreparingArrays = Get-Date - Write-Host "Preparing Arrays duration: $((New-TimeSpan -Start $startPreparingArrays -End $endPreparingArrays).TotalMinutes) minutes ($((New-TimeSpan -Start $startPreparingArrays -End $endPreparingArrays).TotalSeconds) seconds)" -} -function processAADGroups { - if ($NoPIMEligibility) { - Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment exists)' - } - else { - Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment or PIM eligibility exists)' - } - - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving Microsoft Entra groups)" - $startAADGroupsResolveMembers = Get-Date - - $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique - $optimizedTableForAADGroupsQuery = [System.Collections.ArrayList]@() - if ($roleAssignmentsforGroups.Count -gt 0) { - foreach ($roleAssignmentforGroups in $roleAssignmentsforGroups) { - $null = $optimizedTableForAADGroupsQuery.Add($roleAssignmentforGroups) - } - } - - $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount groups from role assignments" - - if (-not $NoPIMEligibility) { - $PIMEligibleGroups = $arrayPIMEligible.where({ $_.IdentityType -eq 'Group' }) | Select-Object IdentityObjectId, IdentityDisplayName | Sort-Object -Property IdentityObjectId -Unique - $cntPIMEligibleGroupsTotal = 0 - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments = 0 - foreach ($PIMEligibleGroup in $PIMEligibleGroups) { - $cntPIMEligibleGroupsTotal++ - if ($optimizedTableForAADGroupsQuery.RoleAssignmentIdentityObjectId -notcontains $PIMEligibleGroup.IdentityObjectId) { - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments++ - $null = $optimizedTableForAADGroupsQuery.Add([PSCustomObject]@{ - RoleAssignmentIdentityObjectId = $PIMEligibleGroup.IdentityObjectId - RoleAssignmentIdentityDisplayname = $PIMEligibleGroup.IdentityDisplayName - }) - } - } - Write-Host " $cntPIMEligibleGroupsTotal groups from PIM eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in role assignments)" - $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count - Write-Host " $aadGroupsCount groups from role assignments and PIM eligibility" - } - - if ($aadGroupsCount -gt 0) { - - switch ($aadGroupsCount) { - { $_ -gt 0 } { $indicator = 1 } - { $_ -gt 10 } { $indicator = 5 } - { $_ -gt 50 } { $indicator = 10 } - { $_ -gt 100 } { $indicator = 20 } - { $_ -gt 250 } { $indicator = 25 } - { $_ -gt 500 } { $indicator = 50 } - { $_ -gt 1000 } { $indicator = 100 } - { $_ -gt 10000 } { $indicator = 250 } - } - - Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" - - $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { - $aadGroupIdWithRoleAssignment = $_ - #region UsingVARs - #fromOtherFunctions - $AADGroupMembersLimit = $using:AADGroupMembersLimit - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $htAADGroupsDetails = $using:htAADGroupsDetails - $arrayGroupRoleAssignmentsOnServicePrincipals = $using:arrayGroupRoleAssignmentsOnServicePrincipals - $arrayGroupRequestResourceNotFound = $using:arrayGroupRequestResourceNotFound - $arrayProgressedAADGroups = $using:arrayProgressedAADGroups - $htAADGroupsExeedingMemberLimit = $using:htAADGroupsExeedingMemberLimit - $indicator = $using:indicator - $htUserTypesGuest = $using:htUserTypesGuest - $htServicePrincipals = $using:htServicePrincipals - #other - $function:getGroupmembers = $using:funcGetGroupmembers - #endregion UsingVARs - - $rndom = Get-Random -Minimum 10 -Maximum 750 - Start-Sleep -Millisecond $rndom - - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)/transitiveMembers/`$count" - $method = 'GET' - $aadGroupMembersCount = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupMembersCountTransitive $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)" -listenOn 'Content' -consistencyLevel 'eventual' - - if ($aadGroupMembersCount -eq 'Request_ResourceNotFound') { - $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ - groupId = $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId - }) - } - else { - if ($aadGroupMembersCount -gt $AADGroupMembersLimit) { - Write-Host " Group exceeding limit ($($AADGroupMembersLimit)); memberCount: $aadGroupMembersCount; Group: $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname) ($($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)); Members will not be resolved adjust the limit using parameter -AADGroupMembersLimit" - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) = @{} - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersAllCount = $aadGroupMembersCount - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersUsersCount = 'n/a' - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersGroupsCount = 'n/a' - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersServicePrincipalsCount = 'n/a' - } - else { - getGroupmembers -aadGroupId $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId -aadGroupDisplayName $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname - } - } - - $null = $script:arrayProgressedAADGroups.Add($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) - $processedAADGroupsCount = $null - $processedAADGroupsCount = ($arrayProgressedAADGroups).Count - if ($processedAADGroupsCount) { - if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" - } - } - } -ThrottleLimit ($ThrottleLimit * 2) - } - else { - Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" - } - - $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count - if ($arrayGroupRequestResourceNotFoundCount -gt 0) { - Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" - } - - Write-Host " processed $($arrayProgressedAADGroups.Count) Microsoft Entra groups" - $endAADGroupsResolveMembers = Get-Date - Write-Host "Resolving Microsoft Entra groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" - Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after resolving Microsoft Entra groups)" -} -function processALZPolicyVersionChecker { - $start = Get-Date - Write-Host "Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data" - $ALZRepositoryURI = 'https://github.com/Azure/Enterprise-Scale.git' - $workingPath = Get-Location - Write-Host " Working directory is '$($workingPath)'" - $ALZFolderName = "ALZ_$(Get-Date -Format $FileTimeStampFormat)" - $ALZPath = "$($OutputPath)/$($ALZFolderName)" - - if (-not (Test-Path -LiteralPath "$($ALZPath)")) { - Write-Host " Creating temporary directory '$($ALZPath)'" - $null = mkdir $ALZPath - } - else { - Write-Host " Unexpected: The path '$($ALZPath)' already exists" - throw - } - - Write-Host " Switching to temporary directory '$($ALZPath)'" - Set-Location $ALZPath - $ALZCloneSuccess = $false - - try { - Write-Host " Try cloning '$($ALZRepositoryURI)'" - git clone $ALZRepositoryURI - if (-not (Test-Path -LiteralPath "$($ALZPath)/Enterprise-Scale" -PathType Container)) { - $ALZCloneSuccess = $false - Write-Host " Cloning '$($ALZRepositoryURI)' failed" - Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" - $script:NoALZPolicyVersionChecker = $true - $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true - Write-Host " Switching back to working directory '$($workingPath)'" - Set-Location $workingPath - } - else { - Write-Host " Cloning '$($ALZRepositoryURI)' succeeded" - $ALZCloneSuccess = $true - } - } - catch { - $_ - Write-Host " Cloning '$($ALZRepositoryURI)' failed" - Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" - $script:NoALZPolicyVersionChecker = $true - $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true - Write-Host " Switching back to working directory '$($workingPath)'" - Set-Location $workingPath - } - - if ($ALZCloneSuccess) { - Write-Host " Switching to directory '$($ALZPath)/Enterprise-Scale'" - Set-Location "$($ALZPath)/Enterprise-Scale" - - $allESLZPolicies = @{} - $allESLZPolicySets = @{} - $allESLZPolicyHashes = @{} - $allESLZPolicySetHashes = @{} - - #Write-Host " Processing ALZ Data Policy definitions" - $gitHist = (git log --format="%ai`t%H`t%an`t%ae`t%s" -- ./eslzArm/managementGroupTemplates/policyDefinitions/dataPolicies.json) | ConvertFrom-Csv -Delimiter "`t" -Header ('Date', 'CommitId', 'Author', 'Email', 'Subject') - $commitCount = 0 - $processDataPolicies = $true - foreach ($commit in $gitHist | Sort-Object -Property Date) { - if ($processDataPolicies) { - if ($commit.CommitId -eq '3476914f9ba9a8f3f641a25497dfb24a4efa1017') { - $processDataPolicies = $false - continue - } - #Write-Host "processing commit (dataPolicies) $($commit.CommitId)" - $commitCount++ - $jsonRaw = git show "$($commit.CommitId):eslzArm/managementGroupTemplates/policyDefinitions/dataPolicies.json" - - $jsonESLZPolicies = $jsonRaw | ConvertFrom-Json - if (($jsonESLZPolicies.variables.policies.policyDefinitions).Count -eq 0) { - } - else { - $eslzPolicies = $jsonESLZPolicies.variables.policies.policyDefinitions - foreach ($policyDefinition in $eslzPolicies) { - $policyJsonConv = ($policyDefinition | ConvertTo-Json -Depth 99) -replace '\[\[', '[' - $policyJsonRebuild = $policyJsonConv | ConvertFrom-Json - $policyJsonRule = $policyJsonRebuild.properties.policyRule | ConvertTo-Json -Depth 99 - $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) - $stringHash = [System.BitConverter]::ToString($hash) - - if (-not $allESLZPolicies.($policyJsonRebuild.name)) { - $allESLZPolicies.($policyJsonRebuild.name) = @{} - $allESLZPolicies.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - $allESLZPolicies.($policyJsonRebuild.name).name = $policyJsonRebuild.name - $allESLZPolicies.($policyJsonRebuild.name).metadataSource = '' - - $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' - } - else { - $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' - - if ($allESLZPolicies.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicies.($policyJsonRebuild.name).$stringHash) { - $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - } - } - - #hsh - if (-not $allESLZPolicyHashes.($stringHash)) { - $allESLZPolicyHashes.($stringHash) = @{} - $allESLZPolicyHashes.($stringHash).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicyHashes.($stringHash).name = $policyJsonRebuild.name - $allESLZPolicyHashes.($stringHash).metadataSource = '' - - $allESLZPolicyHashes.($stringHash).status = 'obsolete' - } - else { - $allESLZPolicyHashes.($stringHash).status = 'obsolete' - if ($allESLZPolicyHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name)) { - $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name - } - } - } - } - } - } - - #Write-Host " Processing ALZ Policy and Set definitions" - $gitHist = (git log --format="%ai`t%H`t%an`t%ae`t%s" -- ./eslzArm/managementGroupTemplates/policyDefinitions/policies.json) | ConvertFrom-Csv -Delimiter "`t" -Header ('Date', 'CommitId', 'Author', 'Email', 'Subject') - $commitCount = 0 - $doNewALZPolicyReadingApproach = $false - foreach ($commit in $gitHist | Sort-Object -Property Date) { - - if ($commit.CommitId -eq '3476914f9ba9a8f3f641a25497dfb24a4efa1017') { - $doNewALZPolicyReadingApproach = $true - } - #Write-Host "processing commit $($commit.CommitId) - doNewALZPolicyReadingApproach: $doNewALZPolicyReadingApproach" - $commitCount++ - - $jsonRaw = git show "$($commit.CommitId):eslzArm/managementGroupTemplates/policyDefinitions/policies.json" - - if ($doNewALZPolicyReadingApproach) { - $jsonESLZPolicies = $jsonRaw -replace '\[\[', '[' | ConvertFrom-Json - [regex]$extractVariableName = "(?<=\[variables\(')[^']+" - $refsPolicyDefinitionsAll = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.All).Value - $refsPolicyDefinitionsAzureCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.AzureCloud).Value - $refsPolicyDefinitionsAzureChinaCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.AzureChinaCloud).Value - $refsPolicyDefinitionsAzureUSGovernment = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.AzureUSGovernment).Value - $refsPolicySetDefinitionsAll = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.All).Value - $refsPolicySetDefinitionsAzureCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.AzureCloud).Value - $refsPolicySetDefinitionsAzureChinaCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.AzureChinaCloud).Value - $refsPolicySetDefinitionsAzureUSGovernment = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.AzureUSGovernment).Value - $listPolicyDefinitionsAzureCloud = $refsPolicyDefinitionsAll + $refsPolicyDefinitionsAzureCloud - $listPolicyDefinitionsAzureChinaCloud = $refsPolicyDefinitionsAll + $refsPolicyDefinitionsAzureChinaCloud - $listPolicyDefinitionsAzureUSGovernment = $refsPolicyDefinitionsAll + $refsPolicyDefinitionsAzureUSGovernment - $listPolicySetDefinitionsAzureCloud = $refsPolicySetDefinitionsAll + $refsPolicySetDefinitionsAzureCloud - $listPolicySetDefinitionsAzureChinaCloud = $refsPolicySetDefinitionsAll + $refsPolicySetDefinitionsAzureChinaCloud - $listPolicySetDefinitionsAzureUSGovernment = $refsPolicySetDefinitionsAll + $refsPolicySetDefinitionsAzureUSGovernment - $policyDefinitionsAzureCloud = $listPolicyDefinitionsAzureCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) - $policyDefinitionsAzureChinaCloud = $listPolicyDefinitionsAzureChinaCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) - $policyDefinitionsAzureUSGovernment = $listPolicyDefinitionsAzureUSGovernment.ForEach({ $jsonESLZPolicies.variables.$_ }) - $policySetDefinitionsAzureCloud = $listPolicySetDefinitionsAzureCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) - $policySetDefinitionsAzureChinaCloud = $listPolicySetDefinitionsAzureChinaCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) - $policySetDefinitionsAzureUSGovernment = $listPolicySetDefinitionsAzureUSGovernment.ForEach({ $jsonESLZPolicies.variables.$_ }) - - switch ($azAPICallConf['checkContext'].Environment.Name) { - 'Azurecloud' { - $policyDefinitionsData = $policyDefinitionsAzureCloud - $policySetDefinitionsData = $policySetDefinitionsAzureCloud - } - 'AzureChinaCloud' { - $policyDefinitionsData = $policyDefinitionsAzureChinaCloud - $policySetDefinitionsData = $policySetDefinitionsAzureChinaCloud - } - 'AzureUSGovernment' { - $policyDefinitionsData = $policyDefinitionsAzureUSGovernment - $policySetDefinitionsData = $policySetDefinitionsAzureUSGovernment - } - } - - foreach ($policyDefinition in $policyDefinitionsData) { - - $policyJsonRebuild = $policyDefinition | ConvertFrom-Json - $policyJsonRule = $policyJsonRebuild.properties.policyRule | ConvertTo-Json -Depth 99 - $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) - $stringHash = [System.BitConverter]::ToString($hash) - - if (-not $allESLZPolicies.($policyJsonRebuild.name)) { - $allESLZPolicies.($policyJsonRebuild.name) = @{} - $allESLZPolicies.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - $allESLZPolicies.($policyJsonRebuild.name).name = $policyJsonRebuild.name - $allESLZPolicies.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' - } - $allESLZPolicies.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($allESLZPolicies.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicies.($policyJsonRebuild.name).$stringHash) { - $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - } - } - - #hsh - if (-not $allESLZPolicyHashes.($stringHash)) { - $allESLZPolicyHashes.($stringHash) = @{} - $allESLZPolicyHashes.($stringHash).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicyHashes.($stringHash).name = $policyJsonRebuild.name - $allESLZPolicyHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicyHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicyHashes.($stringHash).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicyHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicyHashes.($stringHash).status = 'obsolete' - } - $allESLZPolicyHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($allESLZPolicyHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name)) { - $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name - } - } - } - - foreach ($policySetDefinition in $policySetDefinitionsData) { - - $policyJsonRebuild = $policySetDefinition | ConvertFrom-Json - $policyJsonParameters = $policyJsonRebuild.properties.parameters | ConvertTo-Json -Depth 99 - $policyJsonPolicyDefinitions = $policyJsonRebuild.properties.policyDefinitions | ConvertTo-Json -Depth 99 - $hashParameters = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonParameters)) - $stringHashParameters = [System.BitConverter]::ToString($hashParameters) - $hashPolicyDefinitions = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonPolicyDefinitions)) - $stringHashPolicyDefinitions = [System.BitConverter]::ToString($hashPolicyDefinitions) - $stringHash = "$($stringHashParameters)_$($stringHashPolicyDefinitions)" - - if (-not $allESLZPolicySets.($policyJsonRebuild.name)) { - $allESLZPolicySets.($policyJsonRebuild.name) = @{} - $allESLZPolicySets.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - $allESLZPolicySets.($policyJsonRebuild.name).name = $policyJsonRebuild.name - $allESLZPolicySets.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' - } - $allESLZPolicySets.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($allESLZPolicySets.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicySets.($policyJsonRebuild.name).$stringHash) { - $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - } - } - - #hsh - if (-not $allESLZPolicySetHashes.($stringHash)) { - $allESLZPolicySetHashes.($stringHash) = @{} - $allESLZPolicySetHashes.($stringHash).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicySetHashes.($stringHash).name = $policyJsonRebuild.name - $allESLZPolicySetHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySetHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicySetHashes.($stringHash).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySetHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicySetHashes.($stringHash).status = 'obsolete' - } - $allESLZPolicySetHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source - if ($allESLZPolicySetHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name)) { - $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name - } - } - } - } - else { - $jsonESLZPolicies = $jsonRaw | ConvertFrom-Json - if (($jsonESLZPolicies.variables.policies.policyDefinitions).Count -eq 0) { - } - else { - - $eslzPolicies = $jsonESLZPolicies.variables.policies.policyDefinitions - foreach ($policyDefinition in $eslzPolicies) { - $policyJsonConv = ($policyDefinition | ConvertTo-Json -Depth 99) -replace '\[\[', '[' - $policyJsonRebuild = $policyJsonConv | ConvertFrom-Json - $policyJsonRule = $policyJsonRebuild.properties.policyRule | ConvertTo-Json -Depth 99 - $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) - $stringHash = [System.BitConverter]::ToString($hash) - - if (-not $allESLZPolicies.($policyJsonRebuild.name)) { - $allESLZPolicies.($policyJsonRebuild.name) = @{} - $allESLZPolicies.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - $allESLZPolicies.($policyJsonRebuild.name).name = $policyJsonRebuild.name - $allESLZPolicies.($policyJsonRebuild.name).metadataSource = '' - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' - } - if ($allESLZPolicies.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicies.($policyJsonRebuild.name).$stringHash) { - $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - } - } - - #hsh - if (-not $allESLZPolicyHashes.($stringHash)) { - $allESLZPolicyHashes.($stringHash) = @{} - $allESLZPolicyHashes.($stringHash).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicyHashes.($stringHash).name = $policyJsonRebuild.name - $allESLZPolicyHashes.($stringHash).metadataSource = '' - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicyHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicyHashes.($stringHash).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicyHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicyHashes.($stringHash).status = 'obsolete' - } - if ($allESLZPolicyHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name)) { - $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name - } - } - } - - $eslzPolicySets = $jsonESLZPolicies.variables.initiatives.policySetDefinitions - foreach ($policySetDefinition in $eslzPolicySets) { - - $policyJsonConv = ($policySetDefinition | ConvertTo-Json -Depth 99) -replace '\[\[', '[' - $policyJsonRebuild = $policyJsonConv | ConvertFrom-Json - $policyJsonParameters = $policyJsonRebuild.properties.parameters | ConvertTo-Json -Depth 99 - $policyJsonPolicyDefinitions = $policyJsonRebuild.properties.policyDefinitions | ConvertTo-Json -Depth 99 - $hashParameters = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonParameters)) - $stringHashParameters = [System.BitConverter]::ToString($hashParameters) - $hashPolicyDefinitions = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonPolicyDefinitions)) - $stringHashPolicyDefinitions = [System.BitConverter]::ToString($hashPolicyDefinitions) - $stringHash = "$($stringHashParameters)_$($stringHashPolicyDefinitions)" - - if (-not $allESLZPolicySets.($policyJsonRebuild.name)) { - $allESLZPolicySets.($policyJsonRebuild.name) = @{} - $allESLZPolicySets.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - $allESLZPolicySets.($policyJsonRebuild.name).name = $policyJsonRebuild.name - $allESLZPolicySets.($policyJsonRebuild.name).metadataSource = '' - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' - } - else { - $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' - } - if ($allESLZPolicySets.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicySets.($policyJsonRebuild.name).$stringHash) { - $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version - } - } - - #hsh - if (-not $allESLZPolicySetHashes.($stringHash)) { - $allESLZPolicySetHashes.($stringHash) = @{} - $allESLZPolicySetHashes.($stringHash).version = [System.Collections.ArrayList]@() - $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - $allESLZPolicySetHashes.($stringHash).name = $policyJsonRebuild.name - $allESLZPolicySetHashes.($stringHash).metadataSource = '' - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySetHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicySetHashes.($stringHash).status = 'obsolete' - } - } - else { - if ($commitCount -eq $gitHist.Count) { - $allESLZPolicySetHashes.($stringHash).status = 'prod' - } - else { - $allESLZPolicySetHashes.($stringHash).status = 'obsolete' - } - if ($allESLZPolicySetHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { - $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) - } - if (-not $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name)) { - $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name - } - } - } - } - } - } - - - Write-Host " $($allESLZPolicies.Keys.Count) Azure Landing Zones (ALZ) Policy definitions ($($allESLZPolicies.Values.where({$_.status -eq 'Prod'}).Count) productive)" - Write-Host " $($allESLZPolicySets.Keys.Count) Azure Landing Zones (ALZ) PolicySet definitions ($($allESLZPolicySets.Values.where({$_.status -eq 'Prod'}).Count) productive)" - - $arrayObsoleteALZPolicies = @( - 'Deny-PublicEndpoint-Aks', - 'Deny-PublicEndpoint-CosmosDB', - 'Deny-PublicEndpoint-KeyVault', - 'Deny-PublicEndpoint-MySQL', - 'Deny-PublicEndpoint-PostgreSql', - 'Deny-PublicEndpoint-Sql', - 'Deny-PublicEndpoint-Storage', - 'Deploy-ASC-Standard', - 'Deploy-Diagnostics-ActivityLog', - 'Deploy-Diagnostics-AKS', - 'Deploy-Diagnostics-Batch', - 'Deploy-Diagnostics-DataLakeStore', - 'Deploy-Diagnostics-EventHub', - 'Deploy-Diagnostics-KeyVault', - 'Deploy-Diagnostics-LogicAppsWF', - 'Deploy-Diagnostics-PublicIP', - 'Deploy-Diagnostics-RecoveryVault', - 'Deploy-Diagnostics-SearchServices', - 'Deploy-Diagnostics-ServiceBus', - 'Deploy-Diagnostics-SQLDBs', - 'Deploy-Diagnostics-StreamAnalytics', - 'Deploy-DNSZoneGroup-For-Blob-PrivateEndpoint', - 'Deploy-DNSZoneGroup-For-File-PrivateEndpoint', - 'Deploy-DNSZoneGroup-For-KeyVault-PrivateEndpoint', - 'Deploy-DNSZoneGroup-For-Queue-PrivateEndpoint', - 'Deploy-DNSZoneGroup-For-Sql-PrivateEndpoint', - 'Deploy-DNSZoneGroup-For-Table-PrivateEndpoint', - 'Deploy-HUB', - 'Deploy-LA-Config', - 'Deploy-Log-Analytics', - 'Deploy-vHUB', - 'Deploy-vNet', - 'Deploy-vWAN' - ) - foreach ($obsoleteALZPolicy in $arrayObsoleteALZPolicies) { - if (-not $alzPolicies.($obsoleteALZPolicy)) { - $script:alzPolicies.($obsoleteALZPolicy) = @{} - $script:alzPolicies.($obsoleteALZPolicy).latestVersion = '' - $script:alzPolicies.($obsoleteALZPolicy).status = 'obsolete' - $script:alzPolicies.($obsoleteALZPolicy).policyName = $obsoleteALZPolicy - $script:alzPolicies.($obsoleteALZPolicy).metadataSource = '' - } - } - - foreach ($entry in $allESLZPolicies.keys | Sort-Object) { - $thisOne = $allESLZPolicies.($entry) - $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] - $script:alzPolicies.($entry) = @{} - $script:alzPolicies.($entry).latestVersion = $latestVersion - $script:alzPolicies.($entry).status = $thisOne.status - $script:alzPolicies.($entry).policyName = $thisOne.name - $script:alzPolicies.($entry).metadataSource = $thisOne.name - } - - foreach ($entry in $allESLZPolicyHashes.keys | Sort-Object) { - $thisOne = $allESLZPolicyHashes.($entry) - $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] - $script:alzPolicyHashes.($entry) = @{} - $script:alzPolicyHashes.($entry).latestVersion = $latestVersion - $script:alzPolicyHashes.($entry).status = $thisOne.status - $script:alzPolicyHashes.($entry).policyName = $thisOne.name - $script:alzPolicyHashes.($entry).metadataSource = $thisOne.metadataSource - } - - $script:alzPolicySets.'Deploy-Diag-LogAnalytics' = @{} - $script:alzPolicySets.'Deploy-Diag-LogAnalytics'.latestVersion = '1.0.0' - $script:alzPolicySets.'Deploy-Diag-LogAnalytics'.status = 'obsolete' - $script:alzPolicySets.'Deploy-Diag-LogAnalytics'.policySetName = 'Deploy-Diag-LogAnalytics' - foreach ($entry in $allESLZPolicySets.keys | Sort-Object) { - $thisOne = $allESLZPolicySets.($entry) - $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] - $script:alzPolicySets.($entry) = @{} - $script:alzPolicySets.($entry).latestVersion = $latestVersion - $script:alzPolicySets.($entry).status = $thisOne.status - $script:alzPolicySets.($entry).policySetName = $thisOne.name - $script:alzPolicySets.($entry).metadataSource = $thisOne.metadataSource - } - - foreach ($entry in $allESLZPolicySetHashes.keys | Sort-Object) { - $thisOne = $allESLZPolicySetHashes.($entry) - $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] - $script:alzPolicySetHashes.($entry) = @{} - $script:alzPolicySetHashes.($entry).latestVersion = $latestVersion - $script:alzPolicySetHashes.($entry).status = $thisOne.status - $script:alzPolicySetHashes.($entry).policySetName = $thisOne.name - $script:alzPolicySetHashes.($entry).metadataSource = $thisOne.metadataSource - } - - Write-Host " Switching back to working directory '$($workingPath)'" - Set-Location $workingPath - - Write-Host " Removing temporary directory '$($ALZPath)'" - Remove-Item -Recurse -Force $ALZPath - } - - $end = Get-Date - Write-Host " Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" -} -function processApplications { - Write-Host 'Processing Service Principals - Applications' - $script:servicePrincipalsOfTypeApplication = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq 'Application' -and $htServicePrincipals.($_).appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Subscription.TenantId } ) - if ($azAPICallConf['htParameters'].userType -eq 'Guest') { - #checking if Guest has enough permissions - $app4Test = $htServicePrincipals.($servicePrincipalsOfTypeApplication[0]) - $currentTask = "getApp Test $($app4Test.appId)" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($app4Test.appId)'" - $method = 'GET' - $testGetApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - if ($testGetApplication -eq 'skipApplications') { - $skipApplications = $true - Write-Host ' Guest account does not have enough permissions, skipping Applications (Secrets & Certificates)' - } - } - if (-not $skipApplications) { - $startSPApp = Get-Date - $currentDateUTC = (Get-Date).ToUniversalTime() - $script:arrayApplicationRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) - $servicePrincipalsOfTypeApplication | ForEach-Object -Parallel { - - #region UsingVARs - $currentDateUTC = $using:currentDateUTC - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $arrayApplicationRequestResourceNotFound = $using:arrayApplicationRequestResourceNotFound - $htAppDetails = $using:htAppDetails - $htServicePrincipals = $using:htServicePrincipals - #endregion UsingVARs - - $sp = $htServicePrincipals.($_) - - $currentTask = "getApp $($sp.appId)" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" - $method = 'GET' - $getApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - if ($getApplication -eq 'Request_ResourceNotFound') { - $null = $script:arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ - appId = $sp.appId - }) - } - else { - if (($getApplication).Count -eq 0) { - Write-Host "$($sp.appId) no data returned / seems non existent?" - } - else { - $script:htAppDetails.($sp.id) = @{} - $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType - $script:htAppDetails.($sp.id).spGraphDetails = $sp - $script:htAppDetails.($sp.id).appGraphDetails = $getApplication - - $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count - if ($appPasswordCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount - $appPasswordCredentialsExpiredCount = 0 - $appPasswordCredentialsGracePeriodExpiryCount = 0 - $appPasswordCredentialsExpiryOKCount = 0 - $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appPasswordCredential in $getApplication.passwordCredentials) { - $passwordExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays - if ($passwordExpiryTotalDays -lt 0) { - $appPasswordCredentialsExpiredCount++ - } - elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appPasswordCredentialsGracePeriodExpiryCount++ - } - else { - if ($passwordExpiryTotalDays -gt 730) { - $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ - } - else { - $appPasswordCredentialsExpiryOKCount++ - } - } - } - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount - $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount - } - - $appKeyCredentialsCount = ($getApplication.keyCredentials).count - if ($appKeyCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount - $appKeyCredentialsExpiredCount = 0 - $appKeyCredentialsGracePeriodExpiryCount = 0 - $appKeyCredentialsExpiryOKCount = 0 - $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appKeyCredential in $getApplication.keyCredentials) { - $keyCredentialExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays - if ($keyCredentialExpiryTotalDays -lt 0) { - $appKeyCredentialsExpiredCount++ - } - elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appKeyCredentialsGracePeriodExpiryCount++ - } - else { - if ($keyCredentialExpiryTotalDays -gt 730) { - $appKeyCredentialsExpiryOKMoreThan2YearsCount++ - } - else { - $appKeyCredentialsExpiryOKCount++ - } - } - } - $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount - $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount - } - } - } - - } -ThrottleLimit ($ThrottleLimit * 2) - - $endSPApp = Get-Date - Write-Host "Processing Service Principals - Applications duration: $((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalSeconds) seconds)" - } -} -function processDataCollection { - [CmdletBinding()]Param( - [string]$mgId - ) - - Write-Host ' CustomDataCollection ManagementGroups' - $startMgLoop = Get-Date - - $allManagementGroupsFromEntitiesChildOfRequestedMg = $arrayEntitiesFromAPI.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and ($_.Name -eq $mgId -or $_.properties.parentNameChain -contains $mgId) }) - $allManagementGroupsFromEntitiesChildOfRequestedMgCount = ($allManagementGroupsFromEntitiesChildOfRequestedMg).Count - Write-Host " $allManagementGroupsFromEntitiesChildOfRequestedMgCount Management Groups: $(($allManagementGroupsFromEntitiesChildOfRequestedMg.Name | Sort-Object) -join ', ')" - $mgBatch = ($allManagementGroupsFromEntitiesChildOfRequestedMg | Group-Object -Property { ($_.properties.parentNameChain).Count }) | Sort-Object -Property Name - Write-Host " $(($mgBatch | Measure-Object).Count) batches of Management Groups to process:" - - $btchCnt = 0 - foreach ($btch in $mgBatch) { - $btchCnt++ - $listOfMGs = @() - foreach ($btchMg in $btch.Group | Sort-Object -Property name) { - if ($btchMg.name -eq $btchMg.Properties.displayName) { - $listOfMGs += $btchMg.name - } - else { - $listOfMGs += "$($btchMg.name) ($($btchMg.Properties.displayName))" - } - } - Write-Host " Batch#$($btchCnt) - $($listOfMGs.Count) Management Groups: $($listOfMGs -join ', ')" - } - - foreach ($batchLevel in $mgBatch) { - Write-Host " Processing Management Groups L$($batchLevel.Name) ($($batchLevel.Count) Management Groups)" - - showMemoryUsage - - $batchLevel.Group | ForEach-Object -Parallel { - $mgdetail = $_ - #region UsingVARs - #Parameters MG&Sub related - $CsvDelimiter = $using:CsvDelimiter - $CsvDelimiterOpposite = $using:CsvDelimiterOpposite - $ManagementGroupId = $using:ManagementGroupId - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $newTable = $using:newTable - $customDataCollectionDuration = $using:customDataCollectionDuration - $htSubscriptionTagList = $using:htSubscriptionTagList - $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource - $htAllTagList = $using:htAllTagList - $htSubscriptionTags = $using:htSubscriptionTags - $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy - $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet - $htCacheDefinitionsRole = $using:htCacheDefinitionsRole - $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint - $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy - $htCachePolicyComplianceMG = $using:htCachePolicyComplianceMG - $htCachePolicyComplianceResponseTooLargeMG = $using:htCachePolicyComplianceResponseTooLargeMG - $htCacheAssignmentsRole = $using:htCacheAssignmentsRole - $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources - $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint - $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy - $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions - $htManagementGroupsMgPath = $using:htManagementGroupsMgPath - $LimitPOLICYPolicyDefinitionsScopedManagementGroup = $using:LimitPOLICYPolicyDefinitionsScopedManagementGroup - $LimitPOLICYPolicySetDefinitionsScopedManagementGroup = $using:LimitPOLICYPolicySetDefinitionsScopedManagementGroup - $LimitPOLICYPolicyAssignmentsManagementGroup = $using:LimitPOLICYPolicyAssignmentsManagementGroup - $LimitPOLICYPolicySetAssignmentsManagementGroup = $using:LimitPOLICYPolicySetAssignmentsManagementGroup - $LimitRBACRoleAssignmentsManagementGroup = $using:LimitRBACRoleAssignmentsManagementGroup - $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI - $allManagementGroupsFromEntitiesChildOfRequestedMgCount = $using:allManagementGroupsFromEntitiesChildOfRequestedMgCount - $arrayDataCollectionProgressMg = $using:arrayDataCollectionProgressMg - $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub - $htMgAtScopePolicyAssignments = $using:htMgAtScopePolicyAssignments - $htMgAtScopePoliciesScoped = $using:htMgAtScopePoliciesScoped - $htMgAtScopeRoleAssignments = $using:htMgAtScopeRoleAssignments - $htMgASCSecureScore = $using:htMgASCSecureScore - $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention - $htNamingValidation = $using:htNamingValidation - $htPrincipals = $using:htPrincipals - $htServicePrincipals = $using:htServicePrincipals - $htUserTypesGuest = $using:htUserTypesGuest - $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM - $alzPolicies = $using:alzPolicies - $alzPolicySets = $using:alzPolicySets - $alzPolicyHashes = $using:alzPolicyHashes - $alzPolicySetHashes = $using:alzPolicySetHashes - $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances - $ValidPolicyEffects = $using:ValidPolicyEffects - #other - $function:addRowToTable = $using:funcAddRowToTable - $function:namingValidation = $using:funcNamingValidation - $function:resolveObjectIds = $using:funcResolveObjectIds - $function:testGuid = $using:funcTestGuid - - $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore - $function:dataCollectionDiagnosticsMG = $using:funcDataCollectionDiagnosticsMG - $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates - $function:dataCollectionBluePrintDefinitionsMG = $using:funcDataCollectionBluePrintDefinitionsMG - $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions - $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions - $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions - $function:dataCollectionPolicyAssignmentsMG = $using:funcDataCollectionPolicyAssignmentsMG - $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions - $function:dataCollectionRoleAssignmentsMG = $using:funcDataCollectionRoleAssignmentsMG - $function:detectPolicyEffect = $using:funcDetectPolicyEffect - - #endregion usingVARS - $builtInPolicyDefinitionsCount = $using:builtInPolicyDefinitionsCount - - $addRowToTableDone = $false - - $MgDetailThis = $htManagementGroupsMgPath.($mgdetail.Name) - $MgParentId = $MgDetailThis.Parent - $hierarchyLevel = $MgDetailThis.ParentNameChainCount - - if ($MgParentId -eq '__TenantRoot__') { - $MgParentId = 'TenantRoot' - $MgParentName = $MgParentId - } - else { - $MgParentName = $htManagementGroupsMgPath.($MgParentId).DisplayName - } - - $rndom = Get-Random -Minimum 10 -Maximum 750 - Start-Sleep -Millisecond $rndom - $startMgLoopThis = Get-Date - - if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { - - #namingValidation - if (-not [string]::IsNullOrEmpty($mgdetail.properties.displayName)) { - $namingValidationResult = NamingValidation -toCheck $mgdetail.properties.displayName - if ($namingValidationResult.Count -gt 0) { - $script:htNamingValidation.ManagementGroup.($mgdetail.Name) = @{} - $script:htNamingValidation.ManagementGroup.($mgdetail.Name).nameInvalidChars = ($namingValidationResult -join '') - $script:htNamingValidation.ManagementGroup.($mgdetail.Name).name = $mgdetail.properties.displayName - } - } - - $targetMgOrSub = 'MG' - $baseParameters = @{ - scopeId = $mgdetail.Name - scopeDisplayName = $mgdetail.properties.displayName - } - - #ManagementGroupASCSecureScore - $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $mgdetail.Name - - $addRowToTableParameters = @{ - hierarchyLevel = $hierarchyLevel - mgParentId = $mgParentId - mgParentName = $mgParentName - mgAscSecureScoreResult = $mgAscSecureScoreResult - } - - #mg diag - DataCollectionDiagnosticsMG @baseParameters - - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - #MGPolicyCompliance - DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub - } - - #MGBlueprintDefinitions - $functionReturn = DataCollectionBluePrintDefinitionsMG @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } - - #MGPolicyExemptions - DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub - - #MGPolicyDefinitions - $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' - - #MGPolicySetDefinitions - $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' - - if (-not $htMgAtScopePoliciesScoped.($mgdetail.Name)) { - $script:htMgAtScopePoliciesScoped.($mgdetail.Name) = @{} - $script:htMgAtScopePoliciesScoped.($mgdetail.Name).ScopedCount = $policyDefinitionsScopedCount + $policySetDefinitionsScopedCount - } - - $scopedPolicyCounts = @{ - policyDefinitionsScopedCount = $policyDefinitionsScopedCount - policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount - } - - #MgPolicyAssignments - $functionReturn = DataCollectionPolicyAssignmentsMG @baseParameters @addRowToTableParameters @scopedPolicyCounts - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } - - #MGRoleDefinitions - DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - - #MGRoleAssignments - $functionReturn = DataCollectionRoleAssignmentsMG @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } - - if ($addRowToTableDone -ne $true) { - addRowToTable ` - -level $hierarchyLevel ` - -mgName $mgdetail.properties.displayName ` - -mgId $mgdetail.Name ` - -mgParentId $mgParentId ` - -mgParentName $mgParentName ` - -mgASCSecureScore $mgAscSecureScoreResult - } - } - else { - addRowToTable ` - -level $hierarchyLevel ` - -mgName $mgdetail.properties.displayName ` - -mgId $mgdetail.Name ` - -mgParentId $mgParentId ` - -mgParentName $mgParentName ` - -mgASCSecureScore $mgAscSecureScoreResult - } - - - $endMgLoopThis = Get-Date - $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ - Type = 'Mg' - Id = $mgdetail.Name - DurationSec = (New-TimeSpan -Start $startMgLoopThis -End $endMgLoopThis).TotalSeconds - }) - - $null = $script:arrayDataCollectionProgressMg.Add($mgdetail.Name) - $progressCount = ($arrayDataCollectionProgressMg).Count - Write-Host " $($progressCount)/$($allManagementGroupsFromEntitiesChildOfRequestedMgCount) Management Groups processed" - - } -ThrottleLimit $ThrottleLimit - } - - $endMgLoop = Get-Date - Write-Host " CustomDataCollection ManagementGroups processing duration: $((New-TimeSpan -Start $startMgLoop -End $endMgLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startMgLoop -End $endMgLoop).TotalSeconds) seconds)" - - apiCallTracking -stage 'CustomDataCollection ManagementGroups' -spacing ' ' - - #test - if ($builtInPolicyDefinitionsCount -ne ($($htCacheDefinitionsPolicy).Values.where({ $_.Type -eq 'BuiltIn' }).Count) -or $builtInPolicyDefinitionsCount -ne ((($htCacheDefinitionsPolicy).Values.where( { $_.Type -eq 'BuiltIn' } )).Count)) { - Write-Host "$builtInPolicyDefinitionsCount -ne $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'BuiltIn'}).Count) OR $builtInPolicyDefinitionsCount -ne $((($htCacheDefinitionsPolicy).Values.where( {$_.Type -eq 'BuiltIn'} )).Count)" - Write-Host 'Listing all PolicyDefinitions:' - foreach ($tmpPolicyDefinitionId in ($($htCacheDefinitionsPolicy).Keys | Sort-Object)) { - Write-Host $tmpPolicyDefinitionId - } - } - - - #region SUBSCRIPTION - Write-Host ' CustomDataCollection Subscriptions' - - if ($outOfScopeSubscriptions.Count -gt 0) { - Write-Host " CustomDataCollection $($outOfScopeSubscriptions.Count) Subscriptions excluded" -ForegroundColor yellow - $outOfScopeSubscriptionsGroupedByOutOfScopeReason = $outOfScopeSubscriptions | Group-Object -Property outOfScopeReason - foreach ($exclusionreason in $outOfScopeSubscriptionsGroupedByOutOfScopeReason) { - Write-Host " $($exclusionreason.Count): $($exclusionreason.Name)" - } - } - - Write-Host " CustomDataCollection Subscriptions will process $subsToProcessInCustomDataCollectionCount of $childrenSubscriptionsCount" - - $startSubLoop = Get-Date - if ($subsToProcessInCustomDataCollectionCount -gt 0) { - - $counterBatch = [PSCustomObject] @{ Value = 0 } - $batchSize = 100 - if ($subsToProcessInCustomDataCollectionCount -gt 500) { - $batchSize = 200 - } - Write-Host " Subscriptions Batch size: $batchSize" - - $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } - $batchCnt = 0 - foreach ($batch in $subscriptionsBatch) { - $startBatch = Get-Date - $batchCnt++ - Write-Host " processing Batch #$batchCnt/$(($subscriptionsBatch | Measure-Object).Count) ($(($batch.Group | Measure-Object).Count) Subscriptions)" - showMemoryUsage - - $batch.Group | ForEach-Object -Parallel { - $startSubLoopThis = Get-Date - $childMgSubDetail = $_ - #region UsingVARs - #Parameters MG&Sub related - $CsvDelimiter = $using:CsvDelimiter - $CsvDelimiterOpposite = $using:CsvDelimiterOpposite - #Parameters Sub related - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $newTable = $using:newTable - $storageAccounts = $using:storageAccounts - $resourcesAll = $using:resourcesAll - $resourcesIdsAll = $using:resourcesIdsAll - $resourceGroupsAll = $using:resourceGroupsAll - $customDataCollectionDuration = $using:customDataCollectionDuration - $htSubscriptionsMgPath = $using:htSubscriptionsMgPath - $htManagementGroupsMgPath = $using:htManagementGroupsMgPath - $htResourceProvidersAll = $using:htResourceProvidersAll - $arrayFeaturesAll = $using:arrayFeaturesAll - $htSubscriptionTagList = $using:htSubscriptionTagList - $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource - $htAllTagList = $using:htAllTagList - $htSubscriptionTags = $using:htSubscriptionTags - $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy - $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet - $htCacheDefinitionsRole = $using:htCacheDefinitionsRole - $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint - $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy - $htCachePolicyComplianceSUB = $using:htCachePolicyComplianceSUB - $htCachePolicyComplianceResponseTooLargeSUB = $using:htCachePolicyComplianceResponseTooLargeSUB - $htCacheAssignmentsRole = $using:htCacheAssignmentsRole - $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources - $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint - $htCacheAssignmentsPolicyOnResourceGroupsAndResources = $using:htCacheAssignmentsPolicyOnResourceGroupsAndResources - $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy - $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions - $htResourceLocks = $using:htResourceLocks - $LimitPOLICYPolicyDefinitionsScopedSubscription = $using:LimitPOLICYPolicyDefinitionsScopedSubscription - $LimitPOLICYPolicySetDefinitionsScopedSubscription = $using:LimitPOLICYPolicySetDefinitionsScopedSubscription - $LimitPOLICYPolicyAssignmentsSubscription = $using:LimitPOLICYPolicyAssignmentsSubscription - $LimitPOLICYPolicySetAssignmentsSubscription = $using:LimitPOLICYPolicySetAssignmentsSubscription - $childrenSubscriptionsCount = $using:childrenSubscriptionsCount - $subsToProcessInCustomDataCollectionCount = $using:subsToProcessInCustomDataCollectionCount - $arrayDataCollectionProgressSub = $using:arrayDataCollectionProgressSub - $arraySubResourcesAddArrayDuration = $using:arraySubResourcesAddArrayDuration - $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI - $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI - $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub - $htMgASCSecureScore = $using:htMgASCSecureScore - $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention - $htNamingValidation = $using:htNamingValidation - $htPrincipals = $using:htPrincipals - $htServicePrincipals = $using:htServicePrincipals - $htUserTypesGuest = $using:htUserTypesGuest - $arrayDefenderPlans = $using:arrayDefenderPlans - $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped - $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources - $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit - $arrayPsRule = $using:arrayPsRule - $arrayPSRuleTracking = $using:arrayPSRuleTracking - $htClassicAdministrators = $using:htClassicAdministrators - $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM - $alzPolicies = $using:alzPolicies - $alzPolicySets = $using:alzPolicySets - $alzPolicyHashes = $using:alzPolicyHashes - $alzPolicySetHashes = $using:alzPolicySetHashes - $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances - $htDefenderEmailContacts = $using:htDefenderEmailContacts - $arrayVNets = $using:arrayVNets - $arrayPrivateEndPoints = $using:arrayPrivateEndPoints - $htResourceProvidersRef = $using:htResourceProvidersRef - $arrayPrivateEndPointsFromResourceProperties = $using:arrayPrivateEndPointsFromResourceProperties - $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed - $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes - $arrayAdvisorScores = $using:arrayAdvisorScores - $ValidPolicyEffects = $using:ValidPolicyEffects - #$htResourcesWithProperties = $using:htResourcesWithProperties - #other - $function:addRowToTable = $using:funcAddRowToTable - $function:namingValidation = $using:funcNamingValidation - $function:resolveObjectIds = $using:funcResolveObjectIds - $function:testGuid = $using:funcTestGuid - $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore - $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans - $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub - $function:dataCollectionResources = $using:funcDataCollectionResources - $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts - $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups - $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders - $function:dataCollectionFeatures = $using:funcDataCollectionFeatures - $function:dataCollectionResourceLocks = $using:funcDataCollectionResourceLocks - $function:dataCollectionTags = $using:funcDataCollectionTags - $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates - $function:dataCollectionASCSecureScoreSub = $using:funcDataCollectionASCSecureScoreSub - $function:dataCollectionBluePrintDefinitionsSub = $using:funcDataCollectionBluePrintDefinitionsSub - $function:dataCollectionBluePrintAssignmentsSub = $using:funcDataCollectionBluePrintAssignmentsSub - $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions - $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions - $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions - $function:dataCollectionPolicyAssignmentsSub = $using:funcDataCollectionPolicyAssignmentsSub - $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions - $function:dataCollectionRoleAssignmentsSub = $using:funcDataCollectionRoleAssignmentsSub - $function:dataCollectionClassicAdministratorsSub = $using:funcDataCollectionClassicAdministratorsSub - $function:dataCollectionDefenderEmailContacts = $using:funcDataCollectionDefenderEmailContacts - $function:dataCollectionVNets = $using:funcDataCollectionVNets - $function:dataCollectionPrivateEndpoints = $using:funcDataCollectionPrivateEndpoints - $function:dataCollectionAdvisorScores = $using:funcDataCollectionAdvisorScores - $function:detectPolicyEffect = $using:funcDetectPolicyEffect - #endregion UsingVARs - - $addRowToTableDone = $false - - $childMgSubId = $childMgSubDetail.subscriptionId - $childMgSubDisplayName = $childMgSubDetail.subscriptionName - $hierarchyInfo = $htSubscriptionsMgPath.($childMgSubDetail.subscriptionId) - $hierarchyLevel = $hierarchyInfo.level - $childMgId = $hierarchyInfo.Parent - $childMgDisplayName = $hierarchyInfo.ParentName - $childMgMgPath = $hierarchyInfo.pathDelimited - $childMgParentNameChain = $hierarchyInfo.ParentNameChain - $childMgParentNameChainDelimited = $hierarchyInfo.ParentNameChainDelimited - $childMgParentInfo = $htManagementGroupsMgPath.($childMgId) - $childMgParentId = $childMgParentInfo.Parent - $childMgParentName = $htManagementGroupsMgPath.($childMgParentInfo.Parent).DisplayName - - #namingValidation - if (-not [string]::IsNullOrEmpty($childMgSubDisplayName)) { - $namingValidationResult = NamingValidation -toCheck $childMgSubDisplayName - if ($namingValidationResult.Count -gt 0) { - - $script:htNamingValidation.Subscription.($childMgSubId) = @{} - $script:htNamingValidation.Subscription.($childMgSubId).displayNameInvalidChars = ($namingValidationResult -join '') - $script:htNamingValidation.Subscription.($childMgSubId).displayName = $childMgSubDisplayName - } - } - - if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { - $currentSubscription = $htAllSubscriptionsFromAPI.($childMgSubId).subDetails - $subscriptionQuotaId = $currentSubscription.subscriptionPolicies.quotaId - $subscriptionState = $currentSubscription.state - - $targetMgOrSub = 'Sub' - $baseParameters = @{ - scopeId = $childMgSubId - scopeDisplayName = $childMgSubDisplayName - subscriptionQuotaId = $subscriptionQuotaId - } - - if (-not $azAPICallConf['htParameters'].ManagementGroupsOnly) { - #mgSecureScore - $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $childMgId - - #defenderPlans - $dataCollectionDefenderPlansParameters = @{ - ChildMgMgPath = $childMgMgPath - } - DataCollectionDefenderPlans @baseParameters @dataCollectionDefenderPlansParameters - - #defenderEmailContacts - DataCollectionDefenderEmailContacts @baseParameters - - #advisorScores - $dataCollectionAdvisorScoresParameters = @{ - ChildMgMgPath = $childMgMgPath - } - DataCollectionAdvisorScores @baseParameters @dataCollectionAdvisorScoresParameters - - if (-not $azAPICallConf['htParameters'].NoNetwork) { - #VNets - DataCollectionVNets @baseParameters - #PE - DataCollectionPrivateEndpoints @baseParameters - } - - #diagnostics - $dataCollectionDiagnosticsSubParameters = @{ - ChildMgMgPath = $childMgMgPath - ChildMgId = $childMgId - } - DataCollectionDiagnosticsSub @baseParameters @dataCollectionDiagnosticsSubParameters - - if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { - #resources - $dataCollectionStorageAccountsParameters = @{ - ChildMgMgPath = $childMgMgPath - ChildMgParentNameChainDelimited = $childMgParentNameChainDelimited - } - DataCollectionStorageAccounts @baseParameters @dataCollectionStorageAccountsParameters - } - - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - #resources - $dataCollectionResourcesParameters = @{ - ChildMgMgPath = $childMgMgPath - ChildMgParentNameChainDelimited = $childMgParentNameChainDelimited - } - DataCollectionResources @baseParameters @dataCollectionResourcesParameters - } - - #resourceGroups - DataCollectionResourceGroups @baseParameters - - #resourceProviders - if ($azAPICallConf['htParameters'].NoResourceProvidersAtAll -eq $false) { - DataCollectionResourceProviders @baseParameters - } - - #features - DataCollectionFeatures @baseParameters -MgParentNameChain $childMgParentNameChain - - #resourceLocks - DataCollectionResourceLocks @baseParameters - - #tags - $subscriptionTagsReturn = DataCollectionTags @baseParameters - $subscriptionTags = $subscriptionTagsReturn.subscriptionTags - $subscriptionTagsCount = $subscriptionTagsReturn.subscriptionTagsCount - - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - #SubscriptionPolicyCompliance - DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub - } - - #SubscriptionASCSecureScore - $subscriptionASCSecureScore = DataCollectionASCSecureScoreSub @baseParameters - - $addRowToTableParameters = @{ - hierarchyLevel = $hierarchyLevel - childMgDisplayName = $childMgDisplayName - childMgId = $childMgId - childMgParentId = $childMgParentId - childMgParentName = $childMgParentName - mgAscSecureScoreResult = $mgAscSecureScoreResult - #subscriptionQuotaId = $subscriptionQuotaId - subscriptionState = $subscriptionState - subscriptionASCSecureScore = $subscriptionASCSecureScore - subscriptionTags = $subscriptionTags - subscriptionTagsCount = $subscriptionTagsCount - } - - #SubscriptionBlueprintDefinitions - $functionReturn = DataCollectionBluePrintDefinitionsSub @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } - - #SubscriptionBlueprintAssignments - $functionReturn = DataCollectionBluePrintAssignmentsSub @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } - - #SubscriptionPolicyExemptions - DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub - - #SubscriptionPolicyDefinitions - $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' - - #SubscriptionPolicySets - $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' - - $scopedPolicyCounts = @{ - policyDefinitionsScopedCount = $policyDefinitionsScopedCount - policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount - } - - #SubscriptionPolicyAssignments - $functionReturn = DataCollectionPolicyAssignmentsSub @baseParameters @addRowToTableParameters @scopedPolicyCounts - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } - - #SubscriptionRoleDefinitions - DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - - #SubscriptionRoleAssignments - $functionReturn = DataCollectionRoleAssignmentsSub @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } - - #SubscriptionClassicAdministrators - dataCollectionClassicAdministratorsSub @baseParameters -SubscriptionMgPath $childMgMgPath - } - - if ($addRowToTableDone -ne $true) { - addRowToTable ` - -level $hierarchyLevel ` - -mgName $childMgDisplayName ` - -mgId $childMgId ` - -mgParentId $childMgParentId ` - -mgParentName $childMgParentName ` - -mgASCSecureScore $mgAscSecureScoreResult ` - -Subscription $childMgSubDisplayName ` - -SubscriptionId $childMgSubId ` - -SubscriptionASCSecureScore $subscriptionASCSecureScore - } - } - else { - addRowToTable ` - -level $hierarchyLevel ` - -mgName $childMgDisplayName ` - -mgId $childMgId ` - -mgParentId $childMgParentId ` - -mgParentName $childMgParentName ` - -mgASCSecureScore $mgAscSecureScoreResult ` - -Subscription $childMgSubDisplayName ` - -SubscriptionId $childMgSubId ` - -SubscriptionASCSecureScore $subscriptionASCSecureScore - } - $endSubLoopThis = Get-Date - $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ - Type = 'SUB' - Id = $childMgSubId - DurationSec = (New-TimeSpan -Start $startSubLoopThis -End $endSubLoopThis).TotalSeconds - }) - - $null = $script:arrayDataCollectionProgressSub.Add($childMgSubId) - $progressCount = ($arrayDataCollectionProgressSub).Count - Write-Host " $($progressCount)/$($subsToProcessInCustomDataCollectionCount) Subscriptions processed" - - } -ThrottleLimit $ThrottleLimit - - $endBatch = Get-Date - Write-Host " Batch #$batchCnt processing duration: $((New-TimeSpan -Start $startBatch -End $endBatch).TotalMinutes) minutes ($((New-TimeSpan -Start $startBatch -End $endBatch).TotalSeconds) seconds)" - } - - $endSubLoop = Get-Date - Write-Host " CustomDataCollection Subscriptions processing duration: $((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalSeconds) seconds)" - if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { - if ($arrayPSRuleTracking.Count -gt 0) { - $durationPSRuleTotalSeconds = (($arrayPSRuleTracking.duration | Measure-Object -Sum).Sum) - Write-Host " CustomDataCollection Subscriptions 'PSRule for Azure' processing duration (in sum): $($durationPSRuleTotalSeconds / 60) minutes ($($durationPSRuleTotalSeconds) seconds)" - } - } - #test - Write-Host " built-in PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'BuiltIn'}).Count)" - Write-Host " custom PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'Custom'}).Count)" - Write-Host " all PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.Count)" - } - #endregion SUBSCRIPTION - - $durationDataMG = $customDataCollectionDuration.where( { $_.Type -eq 'MG' } ) - $durationDataSUB = $customDataCollectionDuration.where( { $_.Type -eq 'SUB' } ) - $durationMGAverageMaxMin = ($durationDataMG.DurationSec | Measure-Object -Average -Maximum -Minimum) - $durationSUBAverageMaxMin = ($durationDataSUB.DurationSec | Measure-Object -Average -Maximum -Minimum) - Write-Host "Collecting custom data for $($arrayEntitiesFromAPIManagementGroupsCount) ManagementGroups Avg/Max/Min duration in seconds: Average: $([math]::Round($durationMGAverageMaxMin.Average,4)); Maximum: $([math]::Round($durationMGAverageMaxMin.Maximum,4)); Minimum: $([math]::Round($durationMGAverageMaxMin.Minimum,4))" - Write-Host "Collecting custom data for $($arrayEntitiesFromAPISubscriptionsCount) Subscriptions Avg/Max/Min duration in seconds: Average: $([math]::Round($durationSUBAverageMaxMin.Average,4)); Maximum: $([math]::Round($durationSUBAverageMaxMin.Maximum,4)); Minimum: $([math]::Round($durationSUBAverageMaxMin.Minimum,4))" - - apiCallTracking -stage 'CustomDataCollection ManagementGroups and Subscriptions' -spacing ' ' - - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - - $script:resourcesAllGroupedBySubcriptionId = $resourcesAll | Group-Object -Property subscriptionId - - $totaldurationSubResourcesAddArray = ($arraySubResourcesAddArrayDuration.DurationSec | Measure-Object -Sum).Sum - Write-Host "Collecting custom data total duration writing the subResourcesArray: $totaldurationSubResourcesAddArray seconds" - - if (-not $azAPICallConf['htParameters'].HierarchyMapOnly -and -not $azAPICallConf['htParameters'].ManagementGroupsOnly) { - if (-not $NoCsvExport) { - - #fluctuation - Write-Host 'Process Resource fluctuation' - $start = Get-Date - if (Test-Path -Filter "*$($ManagementGroupId)_ResourcesAll.csv" -LiteralPath "$($outputPath)") { - $startImportPrevious = Get-Date - $doResourceFluctuation = $true - - try { - $previous = Get-ChildItem -Path $outputPath -Filter "*$($ManagementGroupId)_ResourcesAll.csv" | Sort-Object -Descending -Property LastWriteTime | Select-Object -First 1 -ErrorAction Stop - $importPrevious = Import-Csv -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($previous.Name)" -Encoding utf8 -Delimiter $CsvDelimiter | Select-Object -ExpandProperty id - Write-Host " Import previous ($($previous.Name)) duration: $((New-TimeSpan -Start $startImportPrevious -End (Get-Date)).TotalSeconds) seconds" - } - catch { - Write-Host " FAILED: importing previous CSV '$($outputPath)$($DirectorySeparatorChar)$($previous.Name)' OR it does not exist (*$($ManagementGroupId)_ResourcesAll.csv)" - $doResourceFluctuation = $false - } - - if ($doResourceFluctuation) { - #$importPrevious.Count - - #https://gist.github.com/fatherjack/4c91cc6832b8b02d1b7319716a5fba52 - function Compare-StringSet { - <# - .SYNOPSIS - Compare two sets of strings and see the matched and unmatched elements from each input - - .DESCRIPTION - Compares sets of - - .PARAMETER Ref - The reference set of values to be compared - - .PARAMETER Diff - The difference set of values to be compared - - .PARAMETER CaseSensitive - Enables a case-sensitive comparison - - .EXAMPLE - $ref, $dif = @( - , @('a', 'b', 'c') - , @('b', 'c', 'd') - ) - $Sets = Compare-StringSet $ref $dif - $Sets.RefOnly - - $Sets.DiffOnly - - $Sets.Both - - This example sets up two arrays with some similar values and then passes them both to the Compare-StringSet function. the results of this are stored in the variable $Sets. - $Sets is an object that has three properties - RefOnly, DiffOnly, and Both. These are sets of the incoming values where they intersect or not. - - .EXAMPLE - $ref, $dif = @( - , @('tree', 'house', 'football') - , @('dog', 'cat', 'tree', 'house', 'Football') - ) - $Sets = Compare-StringSet $ref $dif -CaseSensitive - $Sets.RefOnly - $Sets.DiffOnly - $Sets.Both - - This example sets up two arrays with some similar values and then passes them both to the Compare-StringSet function using the -CaseSensitive switch. The results of this are stored in the variable $Sets. - $Sets is an object that has three properties - RefOnly, DiffOnly, and Both. - - Because of the -CaseSensitive switch usage 'football' is shown as in RefOnly and 'Football' is shown as in DiffOnly. - - .NOTES - From https://gist.github.com/IISResetMe/57ce7b76e1001974a4f7170e10775875 - #> - - param( - [string[]]$Ref, - [string[]]$Diff, - - [switch]$CaseSensitive - ) - - $Comparer = if ($CaseSensitive) { - [System.StringComparer]::InvariantCulture - } - else { - [System.StringComparer]::InvariantCultureIgnoreCase - } - - $Results = [ordered]@{ - RefOnly = @() - Both = @() - DiffOnly = @() - } - - $temp = [System.Collections.Generic.HashSet[string]]::new($Ref, $Comparer) - $temp.IntersectWith($Diff) - $Results['Both'] = $temp - - #$temp = [System.Collections.Generic.HashSet[string]]::new($Ref, [System.StringComparer]::CurrentCultureIgnoreCase) - $temp = [System.Collections.Generic.HashSet[string]]::new($Ref, $Comparer) - $temp.ExceptWith($Diff) - $Results['RefOnly'] = $temp - - #$temp = [System.Collections.Generic.HashSet[string]]::new($Diff, [System.StringComparer]::CurrentCultureIgnoreCase) - $temp = [System.Collections.Generic.HashSet[string]]::new($Diff, $Comparer) - $temp.ExceptWith($Ref) - $Results['DiffOnly'] = $temp - - return [pscustomobject]$Results - } - - Write-Host " Comparing previous ($($importPrevious.Count)) with latest ($($resourcesIdsAll.Count))" - $start = Get-Date - $x = Compare-StringSet $importPrevious $resourcesIdsAll.id - Write-Host ' unique values in previous (deleted):' $x.RefOnly.Count - Write-Host " values that are contained in previous and latest: $($x.Both.Count)" - Write-Host ' unique values in latest (added):' $x.DiffOnly.Count - $end = Get-Date - Write-Host " Compare previous with latest duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) mins ($((New-TimeSpan -Start $start -End $end).TotalSeconds) sec)" - - $script:arrayResourceFluctuationFinal = [System.Collections.ArrayList]@() - - #ADDED - $arrayAdded = [System.Collections.ArrayList]@() - $arrayAddedAndRemoved = [System.Collections.ArrayList]@() - foreach ($resource in $x.DiffOnly) { - $resourceSplitted = $resource.split('/') - #$resourceSplitted - - $null = $arrayAdded.Add([PSCustomObject]@{ - subscriptionId = $resourceSplitted[2] - resourceType0 = $resourceSplitted[6] - resourceType1 = $resourceSplitted[7] - resourceType2 = $resourceSplitted[9] - resourceType3 = $resourceSplitted[11] - }) - - $subDetails = $htSubscriptionsMgPath.($resourceSplitted[2]) - $null = $arrayAddedAndRemoved.Add([pscustomobject]@{ - action = 'add' - subscriptionId = $resourceSplitted[2] - subscriptionName = $subDetails.displayName - mgPath = $subDetails.pathDelimited - resourceId = $resource - resourceType0 = $resourceSplitted[6] - resourceType1 = $resourceSplitted[7] - resourceType2 = $resourceSplitted[9] - resourceType3 = $resourceSplitted[11] - }) - - if ($resourceSplitted.Count -gt 13) { - Write-Host ' Unforeseen Resource type!' - Write-Host " Please report this Resource type at $($GithubRepository): '$resource'" - } - } - - if ($arrayAdded.Count -gt 0) { - $arrayGroupedByResourceType = $arrayAdded | Group-Object -Property resourceType0, resourceType1, resourceType2, resourceType3 - foreach ($resourceType in $arrayGroupedByResourceType) { - $arrayGroupedBySubscription = $arrayGroupedByResourceType.where({ $_.Name -eq $resourceType.Name }).Group | Group-Object -Property subscriptionId | Select-Object -ExcludeProperty Group - $null = $arrayResourceFluctuationFinal.Add([PSCustomObject]@{ - Event = 'Added' - ResourceType = ($resourceType.Name -replace ', ', '/') - 'Resource count' = $resourceType.Count - 'Subscription count' = ($arrayGroupedBySubscription | Measure-Object).Count - }) - } - } - - #REMOVED - $arrayRemoved = [System.Collections.ArrayList]@() - foreach ($resource in $x.RefOnly) { - $resourceSplitted = $resource.split('/') - #$resourceSplitted - - $null = $arrayRemoved.Add([PSCustomObject]@{ - subscriptionId = $resourceSplitted[2] - resourceType0 = $resourceSplitted[6] - resourceType1 = $resourceSplitted[7] - resourceType2 = $resourceSplitted[9] - resourceType3 = $resourceSplitted[11] - }) - - $subDetails = $htSubscriptionsMgPath.($resourceSplitted[2]) - $null = $arrayAddedAndRemoved.Add([pscustomobject]@{ - action = 'remove' - subscriptionId = $resourceSplitted[2] - subscriptionName = $subDetails.displayName - mgPath = $subDetails.pathDelimited - resourceId = $resource - resourceType0 = $resourceSplitted[6] - resourceType1 = $resourceSplitted[7] - resourceType2 = $resourceSplitted[9] - resourceType3 = $resourceSplitted[11] - }) - - if ($resourceSplitted.Count -gt 13) { - Write-Host ' Unforeseen Resource type!' - Write-Host " Please report this Resource type at $($GithubRepository): '$resource'" - } - } - - if ($arrayRemoved.Count -gt 0) { - $arrayGroupedByResourceType = $arrayRemoved | Group-Object -Property resourceType0, resourceType1, resourceType2, resourceType3 - foreach ($resourceType in $arrayGroupedByResourceType) { - $arrayGroupedBySubscription = $arrayGroupedByResourceType.where({ $_.Name -eq $resourceType.Name }).Group | Group-Object -Property subscriptionId | Select-Object -ExcludeProperty Group - $null = $arrayResourceFluctuationFinal.Add([PSCustomObject]@{ - Event = 'Removed' - ResourceType = ($resourceType.Name -replace ', ', '/') - 'Resource count' = $resourceType.Count - 'Subscription count' = ($arrayGroupedBySubscription | Measure-Object).Count - }) - } - } - } - } - else { - Write-Host " Process Resource fluctuation skipped, no previous output (*$($ManagementGroupId)_ResourcesAll.csv) found" - } - - if ($arrayResourceFluctuationFinal.Count -gt 0 -and $doResourceFluctuation) { - if (-not $NoCsvExport) { - #DataCollection Export of Resource fluctuation - Write-Host " Exporting ResourceFluctuation CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuation.csv'" - $arrayResourceFluctuationFinal | Sort-Object -Property ResourceType | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuation.csv" -Delimiter "$csvDelimiter" -NoTypeInformation - - Write-Host " Exporting ResourceFluctuation detailed CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuationDetailed.csv'" - $arrayAddedAndRemoved | Sort-Object -Property Resource | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuationDetailed.csv" -Delimiter "$csvDelimiter" -NoTypeInformation - } - } - Write-Host "Process Resource fluctuation duration: $((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds) seconds" - - #DataCollection Export of All Resources - if ($resourcesIdsAll.Count -gt 0) { - if (-not $NoCsvExport) { - Write-Host "Exporting ResourcesAll CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv'" - $resourcesIdsAll | Sort-Object -Property id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv" -Delimiter "$csvDelimiter" -NoTypeInformation - } - } - else { - Write-Host "Not Exporting ResourcesAll CSV, as there are $($resourcesIdsAll.Count) resources" - } - } - } - } - - if ($azAPICallConf['htParameters'].LargeTenant -eq $false -or $azAPICallConf['htParameters'].PolicyAtScopeOnly -eq $false -or $azAPICallConf['htParameters'].RBACAtScopeOnly -eq $false) { - if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { - addRowToTable ` - -level (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain | Measure-Object).Count - 1) ` - -mgName $getMgParentName ` - -mgId $getMgParentId ` - -mgParentId "'upperScopes'" ` - -mgParentName 'upperScopes' - } - } - - if ($azAPICallConf['htParameters'].LargeTenant -eq $true -or $azAPICallConf['htParameters'].PolicyAtScopeOnly -eq $true) { - if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { - $currentTask = "Policy assignments ('$($ManagementGroupId)')" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementgroups/$($ManagementGroupId)/providers/Microsoft.Authorization/policyAssignments?`$filter=atScope()&api-version=2021-06-01" - $method = 'GET' - $upperScopesPolicyAssignments = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' - - $upperScopesPolicyAssignments = $upperScopesPolicyAssignments | Where-Object { $_.properties.scope -ne "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" } - $upperScopesPolicyAssignmentsPolicyCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' })).count - $upperScopesPolicyAssignmentsPolicySetCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' })).count - $upperScopesPolicyAssignmentsPolicyAtScopeCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -and $_.Id -match "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" })).count - $upperScopesPolicyAssignmentsPolicySetAtScopeCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' -and $_.Id -match "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" })).count - $upperScopesPolicyAssignmentsPolicyAndPolicySetAtScopeCount = ($upperScopesPolicyAssignmentsPolicyAtScopeCount + $upperScopesPolicyAssignmentsPolicySetAtScopeCount) - foreach ($L0mgmtGroupPolicyAssignment in $upperScopesPolicyAssignments) { - - if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -OR $L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { - if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/') { - $PolicyVariant = 'Policy' - $Id = ($L0mgmtGroupPolicyAssignment.properties.policydefinitionid).ToLower() - $Def = ($htCacheDefinitionsPolicy).($Id) - $PolicyAssignmentScope = $L0mgmtGroupPolicyAssignment.Properties.Scope - #$PolicyAssignmentNotScopes = $L0mgmtGroupPolicyAssignment.Properties.NotScopes -join "$CsvDelimiterOpposite " - $PolicyAssignmentId = ($L0mgmtGroupPolicyAssignment.Id).ToLower() - $PolicyAssignmentName = $L0mgmtGroupPolicyAssignment.Name - $PolicyAssignmentDisplayName = $L0mgmtGroupPolicyAssignment.Properties.DisplayName - if (($L0mgmtGroupPolicyAssignment.Properties.Description).length -eq 0) { - $PolicyAssignmentDescription = 'no description given' - } - else { - $PolicyAssignmentDescription = $L0mgmtGroupPolicyAssignment.Properties.Description - } - - if ($L0mgmtGroupPolicyAssignment.identity) { - $PolicyAssignmentIdentity = $L0mgmtGroupPolicyAssignment.identity.principalId - } - else { - $PolicyAssignmentIdentity = 'n/a' - } - - if ($Def.Type -eq 'Custom') { - $policyDefintionScope = $Def.Scope - $policyDefintionScopeMgSub = $Def.ScopeMgSub - $policyDefintionScopeId = $Def.ScopeId - } - else { - $policyDefintionScope = 'n/a' - $policyDefintionScopeMgSub = 'n/a' - $policyDefintionScopeId = 'n/a' - } - - $assignedBy = 'n/a' - $createdBy = '' - $createdOn = '' - $updatedBy = '' - $updatedOn = '' - if ($L0mgmtGroupPolicyAssignment.properties.metadata) { - if ($L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy) { - $assignedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdBy) { - $createdBy = $L0mgmtGroupPolicyAssignment.properties.metadata.createdBy - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdOn) { - $createdOn = $L0mgmtGroupPolicyAssignment.properties.metadata.createdOn - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy) { - $updatedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn) { - $updatedOn = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn - } - } - - if (($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message) { - $nonComplianceMessage = ($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message - } - else { - $nonComplianceMessage = '' - } - - $formatedPolicyAssignmentParameters = '' - $hlp = $L0mgmtGroupPolicyAssignment.Properties.Parameters - if (-not [string]::IsNullOrEmpty($hlp)) { - $arrayPolicyAssignmentParameters = @() - $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { - "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" - } - $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " - } - - #mgSecureScore - $mgAscSecureScoreResult = '' - - addRowToTable ` - -level (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count - 1) ` - -mgName $getMgParentName ` - -mgId $getMgParentId ` - -mgParentId "'upperScopes'" ` - -mgParentName 'upperScopes' ` - -mgASCSecureScore $mgAscSecureScoreResult ` - -Policy $Def.DisplayName ` - -PolicyDescription $Def.Description ` - -PolicyVariant $PolicyVariant ` - -PolicyType $Def.Type ` - -PolicyCategory $Def.Category ` - -PolicyDefinitionIdGuid (($Def.Id) -replace '.*/') ` - -PolicyDefinitionId $Def.PolicyDefinitionId ` - -PolicyDefintionScope $policyDefintionScope ` - -PolicyDefintionScopeMgSub $policyDefintionScopeMgSub ` - -PolicyDefintionScopeId $policyDefintionScopeId ` - -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedManagementGroup ` - -PolicyDefinitionsScopedCount $PolicyDefinitionsScopedCount ` - -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedManagementGroup ` - -PolicySetDefinitionsScopedCount $PolicySetDefinitionsScopedCount ` - -PolicyDefinitionEffectDefault ($htCacheDefinitionsPolicy).(($Def.PolicyDefinitionId)).effectDefaultValue ` - -PolicyDefinitionEffectFixed ($htCacheDefinitionsPolicy).(($Def.PolicyDefinitionId)).effectFixedValue ` - -PolicyAssignmentScope $PolicyAssignmentScope ` - -PolicyAssignmentScopeMgSubRg 'Mg' ` - -PolicyAssignmentScopeName ($PolicyAssignmentScope -replace '.*/', '') ` - -PolicyAssignmentNotScopes $L0mgmtGroupPolicyAssignment.Properties.NotScopes ` - -PolicyAssignmentId $PolicyAssignmentId ` - -PolicyAssignmentName $PolicyAssignmentName ` - -PolicyAssignmentDisplayName $PolicyAssignmentDisplayName ` - -PolicyAssignmentDescription $PolicyAssignmentDescription ` - -PolicyAssignmentEnforcementMode $L0mgmtGroupPolicyAssignment.Properties.EnforcementMode ` - -PolicyAssignmentNonComplianceMessages $L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages ` - -PolicyAssignmentIdentity $PolicyAssignmentIdentity ` - -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsManagementGroup ` - -PolicyAssignmentCount $upperScopesPolicyAssignmentsPolicyCount ` - -PolicyAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAtScopeCount ` - -PolicyAssignmentParameters $L0mgmtGroupPolicyAssignment.Properties.Parameters ` - -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` - -PolicyAssignmentAssignedBy $assignedBy ` - -PolicyAssignmentCreatedBy $createdBy ` - -PolicyAssignmentCreatedOn $createdOn ` - -PolicyAssignmentUpdatedBy $updatedBy ` - -PolicyAssignmentUpdatedOn $updatedOn ` - -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsManagementGroup ` - -PolicySetAssignmentCount $upperScopesPolicyAssignmentsPolicySetCount ` - -PolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicySetAtScopeCount ` - -PolicyAndPolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAndPolicySetAtScopeCount - } - - if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { - $PolicyVariant = 'PolicySet' - $Id = ($L0mgmtGroupPolicyAssignment.properties.policydefinitionid).ToLower() - $Def = ($htCacheDefinitionsPolicySet).($Id) - $PolicyAssignmentScope = $L0mgmtGroupPolicyAssignment.Properties.Scope - #$PolicyAssignmentNotScopes = $L0mgmtGroupPolicyAssignment.Properties.NotScopes -join "$CsvDelimiterOpposite " - $PolicyAssignmentId = ($L0mgmtGroupPolicyAssignment.Id).ToLower() - $PolicyAssignmentName = $L0mgmtGroupPolicyAssignment.Name - $PolicyAssignmentDisplayName = $L0mgmtGroupPolicyAssignment.Properties.DisplayName - if (($L0mgmtGroupPolicyAssignment.Properties.Description).length -eq 0) { - $PolicyAssignmentDescription = 'no description given' - } - else { - $PolicyAssignmentDescription = $L0mgmtGroupPolicyAssignment.Properties.Description - } - - if ($L0mgmtGroupPolicyAssignment.identity) { - $PolicyAssignmentIdentity = $L0mgmtGroupPolicyAssignment.identity.principalId - } - else { - $PolicyAssignmentIdentity = 'n/a' - } - - if ($Def.Type -eq 'Custom') { - $policyDefintionScope = $Def.Scope - $policyDefintionScopeMgSub = $Def.ScopeMgSub - $policyDefintionScopeId = $Def.ScopeId - } - else { - $policyDefintionScope = 'n/a' - $policyDefintionScopeMgSub = 'n/a' - $policyDefintionScopeId = 'n/a' - } - - $assignedBy = 'n/a' - $createdBy = '' - $createdOn = '' - $updatedBy = '' - $updatedOn = '' - if ($L0mgmtGroupPolicyAssignment.properties.metadata) { - if ($L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy) { - $assignedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdBy) { - $createdBy = $L0mgmtGroupPolicyAssignment.properties.metadata.createdBy - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdOn) { - $createdOn = $L0mgmtGroupPolicyAssignment.properties.metadata.createdOn - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy) { - $updatedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy - } - if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn) { - $updatedOn = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn - } - } - - if (($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message) { - $nonComplianceMessage = ($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message - } - else { - $nonComplianceMessage = '' - } - - $formatedPolicyAssignmentParameters = '' - $hlp = $L0mgmtGroupPolicyAssignment.Properties.Parameters - if (-not [string]::IsNullOrEmpty($hlp)) { - $arrayPolicyAssignmentParameters = @() - $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { - "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" - } - $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " - } - - addRowToTable ` - -level (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count - 1) ` - -mgName $getMgParentName ` - -mgId $getMgParentId ` - -mgParentId "'upperScopes'" ` - -mgParentName 'upperScopes' ` - -mgASCSecureScore $mgAscSecureScoreResult ` - -Policy $Def.DisplayName ` - -PolicyDescription $Def.Description ` - -PolicyVariant $PolicyVariant ` - -PolicyType $Def.Type ` - -PolicyCategory $Def.Category ` - -PolicyDefinitionIdGuid (($Def.Id) -replace '.*/') ` - -PolicyDefinitionId $Def.PolicyDefinitionId ` - -PolicyDefintionScope $policyDefintionScope ` - -PolicyDefintionScopeMgSub $policyDefintionScopeMgSub ` - -PolicyDefintionScopeId $policyDefintionScopeId ` - -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedManagementGroup ` - -PolicyDefinitionsScopedCount $PolicyDefinitionsScopedCount ` - -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedManagementGroup ` - -PolicySetDefinitionsScopedCount $PolicySetDefinitionsScopedCount ` - -PolicyAssignmentScope $PolicyAssignmentScope ` - -PolicyAssignmentScopeMgSubRg 'Mg' ` - -PolicyAssignmentScopeName ($PolicyAssignmentScope -replace '.*/', '') ` - -PolicyAssignmentNotScopes $L0mgmtGroupPolicyAssignment.Properties.NotScopes ` - -PolicyAssignmentId $PolicyAssignmentId ` - -PolicyAssignmentName $PolicyAssignmentName ` - -PolicyAssignmentDisplayName $PolicyAssignmentDisplayName ` - -PolicyAssignmentDescription $PolicyAssignmentDescription ` - -PolicyAssignmentEnforcementMode $L0mgmtGroupPolicyAssignment.Properties.EnforcementMode ` - -PolicyAssignmentNonComplianceMessages $L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages ` - -PolicyAssignmentIdentity $PolicyAssignmentIdentity ` - -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsManagementGroup ` - -PolicyAssignmentCount $upperScopesPolicyAssignmentsPolicyCount ` - -PolicyAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAtScopeCount ` - -PolicyAssignmentParameters $L0mgmtGroupPolicyAssignment.Properties.Parameters ` - -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` - -PolicyAssignmentAssignedBy $assignedBy ` - -PolicyAssignmentCreatedBy $createdBy ` - -PolicyAssignmentCreatedOn $createdOn ` - -PolicyAssignmentUpdatedBy $updatedBy ` - -PolicyAssignmentUpdatedOn $updatedOn ` - -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsManagementGroup ` - -PolicySetAssignmentCount $upperScopesPolicyAssignmentsPolicySetCount ` - -PolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicySetAtScopeCount ` - -PolicyAndPolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAndPolicySetAtScopeCount - } - } - } - } - } - - if ($azAPICallConf['htParameters'].LargeTenant -eq $true -or $azAPICallConf['htParameters'].RBACAtScopeOnly -eq $true) { - - #RoleAssignment API (system metadata e.g. createdOn) - $currentTask = "Role assignments API '$($ManagementGroupId)'" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.Authorization/roleAssignments?api-version=2015-07-01" - $method = 'GET' - $roleAssignmentsFromAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - - if ($roleAssignmentsFromAPI.Count -gt 0) { - $principalsToResolve = @() - $principalsToResolve = foreach ($ra in $roleAssignmentsFromAPI.properties | Sort-Object -Property principalId -Unique) { - if (-not $htPrincipals.($ra.principalId)) { - $ra.principalId - } - } - - if ($principalsToResolve.Count -gt 0) { - ResolveObjectIds -objectIds $principalsToResolve - } - } - - #$upperScopesRoleAssignments = GetRoleAssignments -Scope "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" -scopeDetails "getRoleAssignments upperScopes (Mg)" - $upperScopesRoleAssignments = $roleAssignmentsFromAPI - - $upperScopesRoleAssignmentsLimitUtilization = (($upperScopesRoleAssignments | Where-Object { $_.properties.scope -eq "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" })).count - #tenantLevelRoleAssignments - if (-not $htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments') { - $tenantLevelRoleAssignmentsCount = (($upperScopesRoleAssignments | Where-Object { $_.id -like '/providers/Microsoft.Authorization/roleAssignments/*' })).count - $htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments' = @{} - $htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments'.AssignmentsCount = $tenantLevelRoleAssignmentsCount - } - - foreach ($upperScopesRoleAssignment in $upperScopesRoleAssignments) { - - $roleAssignmentId = ($upperScopesRoleAssignment.id).ToLower() - - if ($upperScopesRoleAssignment.properties.scope -ne "/providers/Microsoft.Management/managementGroups/$ManagementGroupId") { - $roleDefinitionId = $upperScopesRoleAssignment.properties.roleDefinitionId - $roleDefinitionIdGuid = $roleDefinitionId -replace '.*/' - - if (-not ($htCacheDefinitionsRole).($roleDefinitionIdGuid)) { - $roleDefinitionName = "'This roleDefinition likely was deleted although a roleAssignment existed'" - } - else { - $roleDefinitionName = ($htCacheDefinitionsRole).($roleDefinitionIdGuid).Name - } - - if (($htPrincipals.($upperScopesRoleAssignment.properties.principalId).displayName).length -eq 0) { - $roleAssignmentIdentityDisplayname = 'n/a' - } - else { - if ($htPrincipals.($upperScopesRoleAssignment.properties.principalId).type -eq 'User') { - if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { - $roleAssignmentIdentityDisplayname = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).displayName - } - else { - $roleAssignmentIdentityDisplayname = 'scrubbed' - } - } - else { - $roleAssignmentIdentityDisplayname = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).displayName - } - } - if (-not $htPrincipals.($upperScopesRoleAssignment.properties.principalId).signInName) { - $roleAssignmentIdentitySignInName = 'n/a' - } - else { - if ($htPrincipals.($upperScopesRoleAssignment.properties.principalId).type -eq 'User') { - if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { - $roleAssignmentIdentitySignInName = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).signInName - } - else { - $roleAssignmentIdentitySignInName = 'scrubbed' - } - } - else { - $roleAssignmentIdentitySignInName = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).signInName - } - } - $roleAssignmentIdentityObjectId = $upperScopesRoleAssignment.properties.principalId - $roleAssignmentIdentityObjectType = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).type - - $roleAssignmentScope = $upperScopesRoleAssignment.properties.scope - $roleAssignmentScopeName = $roleAssignmentScope -replace '.*/' - $roleAssignmentScopeType = 'MG' - - $roleSecurityCustomRoleOwner = 0 - if (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -eq '*' -and ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions)).length -eq 0 -and ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom -eq $True) { - $roleSecurityCustomRoleOwner = 1 - } - $roleSecurityOwnerAssignmentSP = 0 - if ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Id -eq '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal') -or (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -eq '*' -and ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions)).length -eq 0 -and ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom -eq $True -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal')) { - $roleSecurityOwnerAssignmentSP = 1 - } - - $createdBy = '' - $createdOn = '' - $createdOnUnformatted = $null - $updatedBy = '' - $updatedOn = '' - - if ($upperScopesRoleAssignment.properties.createdBy) { - $createdBy = $upperScopesRoleAssignment.properties.createdBy - } - if ($upperScopesRoleAssignment.properties.createdOn) { - $createdOn = $upperScopesRoleAssignment.properties.createdOn - } - if ($upperScopesRoleAssignment.properties.updatedBy) { - $updatedBy = $upperScopesRoleAssignment.properties.updatedBy - } - if ($upperScopesRoleAssignment.properties.updatedOn) { - $updatedOn = $upperScopesRoleAssignment.properties.updatedOn - } - $createdOnUnformatted = $upperScopesRoleAssignment.properties.createdOn - - if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { - $levelToUse = (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count - 1) - $toUseAsmgName = $getMgParentName - $toUseAsmgId = $getMgParentId - $toUseAsmgParentId = "'upperScopes'" - $toUseAsmgParentName = 'upperScopes' - } - else { - $levelToUse = (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count) - $toUseAsmgName = $selectedManagementGroupId.DisplayName - $toUseAsmgId = $selectedManagementGroupId.Name - $toUseAsmgParentId = 'Tenant' - $toUseAsmgParentName = 'Tenant' - } - - #mgSecureScore - $mgAscSecureScoreResult = '' - - addRowToTable ` - -level $levelToUse ` - -mgName $toUseAsmgName ` - -mgId $toUseAsmgId ` - -mgParentId $toUseAsmgParentId ` - -mgParentName $toUseAsmgParentName ` - -mgASCSecureScore $mgAscSecureScoreResult ` - -RoleDefinitionId $roleDefinitionIdGuid ` - -RoleDefinitionName $roleDefinitionName ` - -RoleIsCustom ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom ` - -RoleAssignableScopes (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).AssignableScopes -join "$CsvDelimiterOpposite ") ` - -RoleActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -join "$CsvDelimiterOpposite ") ` - -RoleNotActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions -join "$CsvDelimiterOpposite ") ` - -RoleDataActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).DataActions -join "$CsvDelimiterOpposite ") ` - -RoleNotDataActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotDataActions -join "$CsvDelimiterOpposite ") ` - -RoleAssignmentIdentityDisplayname $roleAssignmentIdentityDisplayname ` - -RoleAssignmentIdentitySignInName $roleAssignmentIdentitySignInName ` - -RoleAssignmentIdentityObjectId $roleAssignmentIdentityObjectId ` - -RoleAssignmentIdentityObjectType $roleAssignmentIdentityObjectType ` - -RoleAssignmentId $roleAssignmentId ` - -RoleAssignmentScope $roleAssignmentScope ` - -RoleAssignmentScopeName $roleAssignmentScopeName ` - -RoleAssignmentScopeType $roleAssignmentScopeType ` - -RoleAssignmentCreatedBy $createdBy ` - -RoleAssignmentCreatedOn $createdOn ` - -RoleAssignmentCreatedOnUnformatted $createdOnUnformatted ` - -RoleAssignmentUpdatedBy $updatedBy ` - -RoleAssignmentUpdatedOn $updatedOn ` - -RoleAssignmentsLimit $LimitRBACRoleAssignmentsManagementGroup ` - -RoleAssignmentsCount $upperScopesRoleAssignmentsLimitUtilization ` - -RoleSecurityCustomRoleOwner $roleSecurityCustomRoleOwner ` - -RoleSecurityOwnerAssignmentSP $roleSecurityOwnerAssignmentSP ` - -RoleAssignmentPIM 'unknown' - } - } - } -} -function processDefinitionInsights() { - $startDefinitionInsights = Get-Date - Write-Host ' Building DefinitionInsights' - - $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider - $utf8 = New-Object -TypeName System.Text.UTF8Encoding - - #region definitionInsightsAzurePolicy - $htmlDefinitionInsights = [System.Text.StringBuilder]::new() - [void]$htmlDefinitionInsights.AppendLine( @' - -
-'@) - - #policy/policySet preQuery - #region preQuery - $htPolicyWithAssignments = @{} - $htPolicyWithAssignments.policy = @{} - $htPolicyWithAssignments.policySet = @{} - - foreach ($policyOrPolicySet in $arrayPolicyAssignmentsEnriched | Sort-Object -Property PolicyAssignmentId -Unique | Group-Object -Property PolicyId, PolicyVariant) { - $policyOrPolicySetNameSplit = $policyOrPolicySet.name.split(', ') - if ($policyOrPolicySetNameSplit[1] -eq 'Policy') { - #policy - if (-not ($htPolicyWithAssignments).policy.($policyOrPolicySetNameSplit[0])) { - $pscustomObj = [System.Collections.ArrayList]@() - foreach ($entry in $policyOrPolicySet.group) { - $null = $pscustomObj.Add([PSCustomObject]@{ - PolicyAssignmentId = $entry.PolicyAssignmentId - PolicyAssignmentDisplayName = $entry.PolicyAssignmentDisplayName - }) - } - ($htPolicyWithAssignments).policy.($policyOrPolicySetNameSplit[0]) = @{} - ($htPolicyWithAssignments).policy.($policyOrPolicySetNameSplit[0]).Assignments = [array]($pscustomObj) - } - } - else { - #policySet - if (-not ($htPolicyWithAssignments).policySet.($policyOrPolicySetNameSplit[0])) { - $pscustomObj = [System.Collections.ArrayList]@() - foreach ($entry in $policyOrPolicySet.group) { - $null = $pscustomObj.Add([PSCustomObject]@{ - PolicyAssignmentId = $entry.PolicyAssignmentId - PolicyAssignmentDisplayName = $entry.PolicyAssignmentDisplayName - }) - } - ($htPolicyWithAssignments).policySet.($policyOrPolicySetNameSplit[0]) = @{} - ($htPolicyWithAssignments).policySet.($policyOrPolicySetNameSplit[0]).Assignments = [array]($pscustomObj) - } - } - } - - foreach ($customPolicy in $tenantCustomPolicies) { - if ($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId)) { - if (-not ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId)) { - ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId) = @{} - ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments) - } - else { - $array = @() - $array += ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments - $array += $htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments - ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = $array - } - } - } - - foreach ($customPolicySet in $tenantCustomPolicySets) { - if ($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId)) { - if (-not ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId)) { - ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId) = @{} - ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments) - } - else { - $array = @() - $array += ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments - $array += $htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments - ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = $array - } - } - } - #endregion preQuery - - #region definitionInsightsPolicyDefinitions - $startDefinitionInsightsPolicyDefinitions = Get-Date - Write-Host ' processing DefinitionInsights Policy definitions' - ShowMemoryUsage - $tfCount = $tenantAllPoliciesCount - $htmlTableId = 'definitionInsights_Policy' - [void]$htmlDefinitionInsights.AppendLine( @" - -
- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - -
- - -
- - - - - -
- - -
- -
-
- -
- -

Default Management Group docs

Default Management Group docs

- - - - - - - - - - - - - - - - - - - - - -"@) - - $cnter = 0 - $htmlDefinitionInsightshlp = $null - $htmlDefinitionInsightshlp = foreach ($policy in (($htCacheDefinitionsPolicy).Values | Sort-Object @{Expression = { $_.DisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { - - $cnter++ - if ($cnter % 1000 -eq 0) { - Write-Host " $cnter Policy definitions processed" - ShowMemoryUsage - } - - $hasAssignments = 'false' - $assignmentsCount = 0 - $assignmentsDetailed = 'n/a' - - if (($htPolicyWithAssignments).policy.($policy.PolicyDefinitionId)) { - $hasAssignments = 'true' - $assignments = ($htPolicyWithAssignments).policy.($policy.PolicyDefinitionId).Assignments - $assignmentsCount = $assignments.Count - - if ($assignmentsCount -gt 0) { - $arrayAssignmentDetails = @() - $arrayAssignmentDetails = foreach ($assignment in $assignments) { - if ($assignment.PolicyAssignmentDisplayName -eq '') { - $polAssDisplayName = '#no AssignmentName given' - } - else { - $polAssDisplayName = $assignment.PolicyAssignmentDisplayName - } - "$($assignment.PolicyAssignmentId) ($($polAssDisplayName))" - } - $assignmentsDetailed = $arrayAssignmentDetails -join "$CsvDelimiterOpposite " - } - - } - - $roleDefinitionIds = 'n/a' - if ($policy.RoleDefinitionIds -ne 'n/a') { - $arrayRoleDefDetails = @() - $arrayRoleDefDetails = foreach ($roleDef in $policy.RoleDefinitionIds) { - $roleDefIdOnly = $roleDef -replace '.*/' - if (($roleDefIdOnly).Length -ne 36) { - "'INVALID RoleDefId!' ($($roleDefIdOnly))" - } - else { - $roleDefHlp = ($htCacheDefinitionsRole).($roleDefIdOnly) - "'$($roleDefHlp.Name)' ($($roleDefHlp.Id))" - } - } - $roleDefinitionIds = $arrayRoleDefDetails -join "$CsvDelimiterOpposite " - } - - $scopeDetails = 'n/a' - if ($policy.ScopeId -ne 'n/a') { - if ([string]::IsNullOrEmpty($policy.ScopeId)) { - Write-Host "unexpected IsNullOrEmpty - processing: $($policy | ConvertTo-Json -Depth 99)" - } - $scopeDetails = "$($policy.ScopeId) ($($htEntities.($policy.ScopeId).DisplayName))" - } - - $usedInPolicySet = 'false' - $usedInPolicySetCount = 0 - $usedInPolicySets = 'n/a' - - if ($htPoliciesUsedInPolicySets.($policy.PolicyDefinitionId)) { - $usedInPolicySet = 'true' - $usedInPolicySetCount = ($htPoliciesUsedInPolicySets.($policy.PolicyDefinitionId).policySet).Count - $usedInPolicySets = ($htPoliciesUsedInPolicySets.($policy.PolicyDefinitionId).policySet | Sort-Object) -join "$CsvDelimiterOpposite " - } - - $json = $($policy.Json | ConvertTo-Json -Depth 99) - $guid = ([System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($policy.PolicyDefinitionId)))) -replace '-' - @" - - - - - - - - - - - - - - - - - - -"@ - } - [void]$htmlDefinitionInsights.AppendLine($htmlDefinitionInsightshlp) - if ($NoDefinitionInsightsDedicatedHTML) { - $htmlDefinitionInsights | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force - $htmlDefinitionInsights = [System.Text.StringBuilder]::new() - } - [void]$htmlDefinitionInsights.AppendLine( @" - -
JSONPolicyTypeALZCategoryDeprecatedPreviewScope Mg/SubScope Name/IdeffectDefaultValuehasAssignmentsAssignments CountAssignmentsUsedInPolicySetPolicySetsCountPolicySetsRoles
- -
- - -
- -
- - - -
$($policy.Type)$($policy.ALZ)$($policy.Category -replace '<', '<' -replace '>', '>')$($policy.Deprecated)$($policy.Preview)$($policy.ScopeMgSub)$($scopeDetails -replace '<', '<' -replace '>', '>')$($policy.effectDefaultValue)$hasAssignments$assignmentsCount$assignmentsDetailed$usedInPolicySet$usedInPolicySetCount$usedInPolicySets$($roleDefinitionIds -replace '<', '<' -replace '>', '>')
-
- - -"@) - $endDefinitionInsightsPolicyDefinitions = Get-Date - Write-Host " DefinitionInsights Policy definitions duration: $((New-TimeSpan -Start $startDefinitionInsightsPolicyDefinitions -End $endDefinitionInsightsPolicyDefinitions).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsightsPolicyDefinitions -End $endDefinitionInsightsPolicyDefinitions).TotalSeconds) seconds)" - showMemoryUsage - #endregion definitionInsightsPolicyDefinitions - - #region definitionInsightsPolicySetDefinitions - $startDefinitionInsightsPolicySetDefinitions = Get-Date - Write-Host ' processing DefinitionInsights PolicySet definitions' - ShowMemoryUsage - $tfCount = $tenantAllPolicySetsCount - $htmlTableId = 'definitionInsights_PolicySet' - [void]$htmlDefinitionInsights.AppendLine( @" - -
- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
- - - - - - - - - - - - - - - - - - -"@) - $htmlDefinitionInsightshlp = $null - $htmlDefinitionInsightshlp = foreach ($policySet in ($tenantAllPolicySets | Sort-Object @{Expression = { $_.DisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { - $hasAssignments = 'false' - $assignmentsCount = 0 - $assignmentsDetailed = 'n/a' - - if (($htPolicyWithAssignments).policySet.($policySet.PolicyDefinitionId)) { - $hasAssignments = 'true' - $assignments = ($htPolicyWithAssignments).policySet.($policySet.PolicyDefinitionId).Assignments - $assignmentsCount = ($assignments | Measure-Object).Count - - if ($assignmentsCount -gt 0) { - $arrayAssignmentDetails = @() - $arrayAssignmentDetails = foreach ($assignment in $assignments) { - if ($assignment.PolicyAssignmentDisplayName -eq '') { - $polAssDisplayName = '#no AssignmentName given' - } - else { - $polAssDisplayName = $assignment.PolicyAssignmentDisplayName - } - "$($assignment.PolicyAssignmentId) ($($polAssDisplayName))" - } - $assignmentsDetailed = $arrayAssignmentDetails -join "$CsvDelimiterOpposite " - } - } - - $scopeDetails = 'n/a' - if ($policySet.ScopeId -ne 'n/a') { - $scopeDetails = "$($policySet.ScopeId) ($($htEntities.($policySet.ScopeId).DisplayName))" - } - $json = $($policySet.Json | ConvertTo-Json -Depth 99) - $guid = ([System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($policySet.PolicyDefinitionId)))) -replace '-' - @" - - - - - - - - - - - - - -"@ - } - [void]$htmlDefinitionInsights.AppendLine($htmlDefinitionInsightshlp) - if ($NoDefinitionInsightsDedicatedHTML) { - $htmlDefinitionInsights | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force - $htmlDefinitionInsights = [System.Text.StringBuilder]::new() - } - [void]$htmlDefinitionInsights.AppendLine( @" - -
JSONPolicySet TypeALZCategoryDeprecatedPreviewScope Mg/SubScope Name/IdhasAssignmentsAssignments CountAssignments
- -
- - -
- -
- - - -
$($policySet.Type)$($policySet.ALZ)$($policySet.Category -replace '<', '<' -replace '>', '>')$($policySet.Deprecated)$($policySet.Preview)$($policySet.ScopeMgSub)$($scopeDetails -replace '<', '<' -replace '>', '>')$hasAssignments$assignmentsCount$assignmentsDetailed
-
- -
-"@) - $endDefinitionInsightsPolicySetDefinitions = Get-Date - Write-Host " DefinitionInsights PolicySet definitions duration: $((New-TimeSpan -Start $startDefinitionInsightsPolicySetDefinitions -End $endDefinitionInsightsPolicySetDefinitions).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsightsPolicySetDefinitions -End $endDefinitionInsightsPolicySetDefinitions).TotalSeconds) seconds)" - showMemoryUsage - #endregion definitionInsightsPolicySetDefinitions - - [void]$htmlDefinitionInsights.AppendLine( @' - -'@) - #endregion definitionInsightsAzurePolicy - - #region definitionInsightsAzureRBAC - [void]$htmlDefinitionInsights.AppendLine( @' - -
-'@) - - #RBAC preQuery - $htRoleWithAssignments = @{} - foreach ($roleDef in $rbacAll | Sort-Object -Property RoleAssignmentId -Unique | Group-Object -Property RoleId) { - if (-not ($htRoleWithAssignments).($roleDef.Name)) { - ($htRoleWithAssignments).($roleDef.Name) = @{} - ($htRoleWithAssignments).($roleDef.Name).Assignments = $roleDef.group - } - } - - #region definitionInsightsRoleDefinitions - $startDefinitionInsightsRoleDefinitions = Get-Date - Write-Host ' processing DefinitionInsights Role definitions' - ShowMemoryUsage - $tfCount = $tenantAllRolesCount - $htmlTableId = 'definitionInsights_Roles' - [void]$htmlDefinitionInsights.AppendLine( @" - -
- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
- - - - - - - - - - - - - - -"@) - $arrayRoleDefinitionsForCSVExport = [System.Collections.ArrayList]@() - $htmlDefinitionInsightshlp = $null - $htmlDefinitionInsightshlp = foreach ($role in ($tenantAllRoles | Sort-Object @{Expression = { $_.Name } })) { - if ($role.IsCustom -eq $true) { - $roleType = 'Custom' - $AssignableScopesCount = $role.AssignableScopes.Count - if ($role.AssignableScopes -like '*/providers/microsoft.management/managementgroups/*') { - $AssignableScopesMG = $true - } - else { - $AssignableScopesMG = $false - } - - } - else { - $roleType = 'Builtin' - $AssignableScopesCount = '' - $AssignableScopesMG = '' - } - if (-not [string]::IsNullOrEmpty($role.DataActions) -or -not [string]::IsNullOrEmpty($role.NotDataActions)) { - $roleManageData = 'true' - } - else { - $roleManageData = 'false' - } - - $hasAssignments = 'false' - $assignmentsCount = 0 - $assignmentsDetailed = 'n/a' - if (($htRoleWithAssignments).($role.Id)) { - $hasAssignments = 'true' - $assignments = ($htRoleWithAssignments).($role.Id).Assignments - $assignmentsCount = ($assignments).Count - if ($assignmentsCount -gt 0) { - $arrayAssignmentDetails = @() - $arrayAssignmentDetails = foreach ($assignment in $assignments) { - "$($assignment.RoleAssignmentId)" - } - $assignmentsDetailed = $arrayAssignmentDetails -join "$CsvDelimiterOpposite " - } - } - - #array for exportCSV - if (-not $NoCsvExport) { - $null = $arrayRoleDefinitionsForCSVExport.Add([PSCustomObject]@{ - Name = $role.Name - Id = $role.Id - Description = $role.Json.description - Type = $roleType - AssignmentsCount = $assignmentsCount - AssignableScopesCount = $AssignableScopesCount - AssignableScopesMG = $AssignableScopesMG - AssignableScopes = ($role.AssignableScopes | Sort-Object) -join "$CsvDelimiterOpposite " - DataRelated = $roleManageData - RoleAssWriteCapable = $role.RoleCanDoRoleAssignments - Actions = $role.Actions -join "$CsvDelimiterOpposite " - NotActions = $role.NotActions -join "$CsvDelimiterOpposite " - DataActions = $role.DataActions -join "$CsvDelimiterOpposite " - NotDataActions = $role.NotDataActions -join "$CsvDelimiterOpposite " - }) - } - - $json = $role.Json | ConvertTo-Json -Depth 99 - $guid = $role.Id -replace '-' - @" - - - - - - - - - -"@ - } - - #region exportCSV - if (-not $NoCsvExport) { - $csvFilename = "$($filename)_RoleDefinitions" - Write-Host " Exporting RoleDefinitions CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" - $arrayRoleDefinitionsForCSVExport | Sort-Object -Property Type, Name, Id | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation - $arrayRoleDefinitionsForCSVExport = $null - } - #endregion exportCSV - - [void]$htmlDefinitionInsights.AppendLine($htmlDefinitionInsightshlp) - if ($NoDefinitionInsightsDedicatedHTML) { - $htmlDefinitionInsights | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force - $htmlDefinitionInsights = [System.Text.StringBuilder]::new() - } - [void]$htmlDefinitionInsights.AppendLine( @" - -
JSONRole TypeDatacanDoRoleAssignmentshasAssignmentsAssignments CountAssignments
- -
- - -
- -
- - - -
$($roleType)$($roleManageData)$($role.RoleCanDoRoleAssignments)$hasAssignments$assignmentsCount$assignmentsDetailed
-
- -
-"@) - $endDefinitionInsightsRoleDefinitions = Get-Date - Write-Host " DefinitionInsights Role definitions duration: $((New-TimeSpan -Start $startDefinitionInsightsRoleDefinitions -End $endDefinitionInsightsRoleDefinitions).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsightsRoleDefinitions -End $endDefinitionInsightsRoleDefinitions).TotalSeconds) seconds)" - showMemoryUsage - #endregion definitionInsightsRoleDefinitions - - [void]$htmlDefinitionInsights.AppendLine( @' -
-'@) - #endregion definitionInsightsAzureRBAC - - Write-Host " NoDefinitionInsightsDedicatedHTML: $NoDefinitionInsightsDedicatedHTML" - if ($NoDefinitionInsightsDedicatedHTML) { - Write-Host ' Appending DefinitionInsights to HTML' - $script:html += $htmlDefinitionInsights - $htmlDefinitionInsights = $null - $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force - $script:html = $null - } - else { - Write-Host " Creating dedicated DefinitionInsights HTML ($($outputPath)$($DirectorySeparatorChar)$($fileName)_DefinitionInsights.html)" - $htmlDefinitionInsightsDedicated = $null - $htmlDefinitionInsightsDedicated += $htmlDefinitionInsightsDedicatedStart - $htmlDefinitionInsightsDedicated += $htmlDefinitionInsights - $htmlDefinitionInsightsDedicated += $htmlDefinitionInsightsDedicatedEnd - #$htmlDefinitionInsights = $null - $htmlDefinitionInsightsDedicated | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_DefinitionInsights.html" -Encoding utf8 -Force - #$script:htmlDefinitionInsightsDedicated = $null - - $htmlDefinitionInsightsNo = @" - DefinitionInsights has been saved to dedicated HTML file '$($outputPathGiven)$($DirectorySeparatorChar)$($fileName)_DefinitionInsights.html' (parameter -NoDefinitionInsightsDedicatedHTML = $($NoDefinitionInsightsDedicatedHTML))
- Open DefinitionInsights -"@ - $script:html += $htmlDefinitionInsightsNo - #$htmlDefinitionInsightsNo = $null - $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force - $script:html = $null - } - - - $endDefinitionInsights = Get-Date - Write-Host " DefinitionInsights processing duration: $((New-TimeSpan -Start $startDefinitionInsights -End $endDefinitionInsights).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsights -End $endDefinitionInsights).TotalSeconds) seconds)" - ShowMemoryUsage -} -function processDiagramMermaid() { - if ($ManagementGroupId -ne $azAPICallConf['checkContext'].Tenant.Id) { - $optimizedTableForPathQueryMg = $optimizedTableForPathQueryMg.where({ $_.mgParentId -ne "'upperScopes'" }) - } - $mgLevels = ($optimizedTableForPathQueryMg | Sort-Object -Property Level -Unique).Level - - foreach ($mgLevel in $mgLevels) { - $mgsInLevel = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel } )).MgId | Get-Unique - foreach ($mgInLevel in $mgsInLevel) { - $mgDetails = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) - $mgName = $mgDetails.MgName | Get-Unique - $mgParentId = $mgDetails.mgParentId | Get-Unique - $mgParentName = $mgDetails.mgParentName | Get-Unique - if ($mgInLevel -ne $getMgParentId) { - $null = $script:arrayMgs.Add($mgInLevel) - } - - if ($mgParentName -eq $mgParentId) { - $mgParentNameId = $mgParentName - } - else { - $mgParentNameId = "$mgParentName
$mgParentId" - } - - if ($mgName -eq $mgInLevel) { - $mgNameId = $mgName - } - else { - $mgNameId = "$mgName
$mgInLevel" - } - $script:markdownhierarchyMgs += @" -$mgParentId(`"$mgParentNameId`") --> $mgInLevel(`"$mgNameId`")`n -"@ - $subsUnderMg = ($optimizedTableForPathQueryMgAndSub.where( { -not [string]::IsNullOrEmpty($_.SubscriptionId) -and $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )).SubscriptionId - if (($subsUnderMg | Measure-Object).count -gt 0) { - foreach ($subUnderMg in $subsUnderMg) { - $null = $script:arraySubs.Add("SubsOf$mgInLevel") - $mgDetalsN = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) - $mgName = $mgDetalsN.MgName | Get-Unique - $mgParentId = $mgDetalsN.MgParentId | Get-Unique - $mgParentName = $mgDetalsN.MgParentName | Get-Unique - $subName = ($optimizedTableForPathQuery.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel -and $_.SubscriptionId -eq $subUnderMg } )).Subscription | Get-Unique - $script:markdownTable += @" -| $mgLevel | $mgName | $mgInLevel | $mgParentName | $mgParentId | $subName | $($subUnderMg -replace '.*/') |`n -"@ - } - $mgName = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )).MgName | Get-Unique - if ($mgName -eq $mgInLevel) { - $mgNameId = $mgName - } - else { - $mgNameId = "$mgName
$mgInLevel" - } - $script:markdownhierarchySubs += @" -$mgInLevel(`"$mgNameId`") --> SubsOf$mgInLevel(`"$(($subsUnderMg | Measure-Object).count)`")`n -"@ - } - else { - $mgDetailsM = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) - $mgName = $mgDetailsM.MgName | Get-Unique - $mgParentId = $mgDetailsM.MgParentId | Get-Unique - $mgParentName = $mgDetailsM.MgParentName | Get-Unique - $script:markdownTable += @" -| $mgLevel | $mgName | $mgInLevel | $mgParentName | $mgParentId | none | none |`n -"@ - } - - if (($script:outOfScopeSubscriptions | Measure-Object).count -gt 0) { - $subsoosUnderMg = ($outOfScopeSubscriptions | Where-Object { $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel }).SubscriptionId | Get-Unique - if (($subsoosUnderMg | Measure-Object).count -gt 0) { - foreach ($subUnderMg in $subsoosUnderMg) { - $null = $script:arraySubsOos.Add("SubsoosOf$mgInLevel") - $mgDetalsN = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel } )) - $mgName = $mgDetalsN.MgName | Get-Unique - } - $mgName = ($outOfScopeSubscriptions | Where-Object { $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel }).ManagementGroupName | Get-Unique - if ($mgName -eq $mgInLevel) { - $mgNameId = $mgName - } - else { - $mgNameId = "$mgName
$mgInLevel" - } - $script:markdownhierarchySubs += @" -$mgInLevel(`"$mgNameId`") --> SubsoosOf$mgInLevel(`"$(($subsoosUnderMg | Measure-Object).count)`")`n -"@ - } - } - } - } -} -function processHierarchyMapOnly { - foreach ($entity in $htEntities.values) { - if ($entity.parentNameChain -contains $ManagementGroupID -or $entity.Id -eq $ManagementGroupId) { - - if ($entity.type -eq '/subscriptions') { - $hlpEntityParent = $htEntities.(($entity.parent)) - addRowToTable ` - -level (($entity.ParentNameChain).Count - 1) ` - -mgName $hlpEntityParent.displayName ` - -mgId ($entity.parent) ` - -mgParentId $hlpEntityParent.Parent ` - -mgParentName $hlpEntityParent.ParentDisplayName ` - -Subscription $entity.DisplayName ` - -SubscriptionId $entity.Id - } - if ($entity.type -eq 'Microsoft.Management/managementGroups') { - addRowToTable ` - -level ($entity.ParentNameChain).Count ` - -mgName $entity.displayname ` - -mgId $entity.id ` - -mgParentId $entity.Parent ` - -mgParentName $entity.ParentDisplayName - } - } - } -} -function processHierarchyMapOnlyCustomData { - Write-Host 'HierarchyMapOnly with custom data' -ForegroundColor Yellow - Write-Host ' Parameter HierarchyMapOnly:' $HierarchyMapOnly - Write-Host ' Check if HierarchyMapOnlyCustomDataJSON is valid JSON' - try { - $HierarchyMapOnlyCustomDataConvertedAsHashTable = $HierarchyMapOnlyCustomDataJSON | ConvertFrom-Json -AsHashtable - $hierarchyMapOnlyCustomData = @{} - foreach ($key in $HierarchyMapOnlyCustomDataConvertedAsHashTable.Keys) { - $hierarchyMapOnlyCustomData.$key = $HierarchyMapOnlyCustomDataConvertedAsHashTable.$key | ConvertTo-Json | ConvertFrom-Json - } - Write-Host ' HierarchyMapOnlyCustomDataJSON is valid JSON' -ForegroundColor Green - } - catch { - throw 'HierarchyMapOnlyCustomDataJSON is not valid JSON' - } - - Write-Host ' Parameter hierarchyMapOnlyCustomData count:' $hierarchyMapOnlyCustomData.Keys.Count - - #validate - Write-Host ' ManagementGroupId validation' - if (-not $ManagementGroupId) { - throw 'ManagementGroupId validation failed - please provide ManagementGroupId (parameter -ManagementGroupId)' - } - else { - if ($hierarchyMapOnlyCustomData.$ManagementGroupId) { - Write-Host " ManagementGroupId '$ManagementGroupId' is available in 'hierarchyMapOnlyCustomData'" - } - else { - throw "ManagementGroupId validation failed - Given ManagementGroupId '$ManagementGroupId' is NOT available in 'hierarchyMapOnlyCustomData'" - } - Write-Host " ManagementGroupId validation passed '$ManagementGroupId'" -ForegroundColor Green - } - - Write-Host ' CustomData validation' - if ($hierarchyMapOnlyCustomData.Keys.Count -gt 0) { - Write-Host ' Checking Keys (sanity check on first item)' - $requiredKeys = @('Id', 'ParentId', 'ParentNameChain', 'ParentDisplayName', 'DisplayName', 'type') - $firstItem = $hierarchyMapOnlyCustomData.($($hierarchyMapOnlyCustomData.Keys)[0]) - foreach ($requiredKey in $requiredKeys) { - if (($firstitem | Get-Member -Name $requiredKey)) { - Write-Host " Key:$($requiredKey) exists" -ForegroundColor Green - } - else { - Write-Host " CustomData validation failed - required key:$($requiredKey) missing" -ForegroundColor DarkRed - Write-Host " The following keys are expected: $($requiredKeys -join ', ')" - throw "CustomData validation failed - required key:$($requiredKey) missing" - } - } - - Write-Host ' Checking for existence of Management Groups' - $HierarchyMapOnlyCustomDataHroupedByType = $hierarchyMapOnlyCustomData.values | Group-Object -Property type - if ($HierarchyMapOnlyCustomDataHroupedByType.Name -notcontains 'Microsoft.Management/managementGroups') { - Write-Host ' CustomData validation failed - Custom data does not contain Manangement Groups' - throw 'CustomData validation failed - Custom data does not contain Manangement Groups' - } - else { - Write-Host ' Checking for existence of Management Groups passed' -ForegroundColor Green - } - foreach ($type in $HierarchyMapOnlyCustomDataHroupedByType) { - Write-Host " Custom Data contains $($type.Count) x type: '$($type.name)'" - } - - Write-Host ' CustomData validation passed' -ForegroundColor Green - } - else { - Write-Host " CustomData validation failed - no data (`$hierarchyMapOnlyCustomData.Keys.Count: $($hierarchyMapOnlyCustomData.Keys.Count))" - throw "CustomData validation failed - no data (`$hierarchyMapOnlyCustomData.Keys.Count: $($hierarchyMapOnlyCustomData.Keys.Count))" - } - $script:htEntities = $hierarchyMapOnlyCustomData -} -function processManagedIdentities { - Write-Host 'Processing Service Principals - Managed Identities' - $startSPMI = Get-Date - $script:servicePrincipalsOfTypeManagedIdentity = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq 'ManagedIdentity' } ) - $script:servicePrincipalsOfTypeManagedIdentityCount = $servicePrincipalsOfTypeManagedIdentity.Count - if ($servicePrincipalsOfTypeManagedIdentityCount -gt 0) { - foreach ($sp in $servicePrincipalsOfTypeManagedIdentity) { - $hlpSp = $htServicePrincipals.($sp) - if ($hlpSp.alternativeNames -gt 0) { - foreach ($usageentry in $hlpSp.alternativeNames) { - if ($usageentry -like '*/providers/Microsoft.Authorization/policyAssignments/*') { - $script:htManagedIdentityForPolicyAssignment.($hlpSp.Id) = @{} - $script:htManagedIdentityForPolicyAssignment.($hlpSp.Id).policyAssignmentId = $usageentry.ToLower() - $script:htPolicyAssignmentManagedIdentity.($usageentry.ToLower()) = @{} - $script:htPolicyAssignmentManagedIdentity.($usageentry.ToLower()).miObjectId = $hlpSp.id - if (-not $htManagedIdentityDisplayName.($hlpSp.displayName)) { - $script:htManagedIdentityDisplayName.("$($hlpSp.displayName)_$($usageentry.ToLower())") = $hlpSp - } - } - } - } - } - } - $endSPMI = Get-Date - Write-Host "Processing Service Principals - Managed Identities duration: $((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalSeconds) seconds)" -} -function processNetwork { - $start = Get-Date - Write-Host "Processing Network enrichment ($($arrayVNets.Count) Virtual Networks)" - - $htVNets = @{} - foreach ($vnet in $arrayVNets) { - $htVNets.($vnet.id) = $vnet - } - - $script:htSubnets = @{} - $script:arrayVirtualNetworks = [System.Collections.ArrayList]@() - $script:arraySubnets = [System.Collections.ArrayList]@() - - foreach ($vnet in $arrayVNets) { - - #region peerings - $vnetIdSplit = ($vnet.id -split '/') - $subscriptionId = $vnetIdSplit[2] - - $subscriptionName = 'n/a' - $MGPath = 'n/a' - if ($htSubscriptionsMgPath.($subscriptionId)) { - $subHelper = $htSubscriptionsMgPath.($subscriptionId) - $subscriptionName = $subHelper.displayName - $MGPath = $subHelper.ParentNameChainDelimited - } - - $subnetsWithPrivateEndPointsCount = 0 - if ($vnet.properties.subnets.properties.privateEndpoints.id.Count -gt 0) { - $subnetsWithPrivateEndPointsCount = $vnet.properties.subnets.where({ $_.properties.privateEndpoints.id.Count -gt 0 }).Count - } - - $subnetsWithConnectedDevicesCount = 0 - if ($vnet.properties.subnets.properties.ipConfigurations.id.Count -gt 0) { - $subnetsWithConnectedDevicesCount = $vnet.properties.subnets.where({ $_.properties.ipConfigurations.id.Count -gt 0 }).Count - } - - $vnetResourceGroup = $vnetIdSplit[4] - if ($vnet.properties.virtualNetworkPeerings.id.Count -gt 0) { - foreach ($peering in $vnet.properties.virtualNetworkPeerings) { - $remotevnetIdSplit = ($peering.properties.remoteVirtualNetwork.id -split '/') - $remotesubscriptionId = $remotevnetIdSplit[2] - - $remotesubscriptionName = 'n/a' - $remoteMGPath = 'n/a' - $peeringXTenant = 'unknown' - if ($htSubscriptionsMgPath.($remotesubscriptionId)) { - $peeringXTenant = 'false' - $remotesubHelper = $htSubscriptionsMgPath.($remotesubscriptionId) - $remotesubscriptionName = $remotesubHelper.displayName - $remoteMGPath = $remotesubHelper.ParentNameChainDelimited - } - else { - if ($htUnknownTenantsForSubscription.($remotesubscriptionId)) { - $remoteTenantId = $htUnknownTenantsForSubscription.($remotesubscriptionId).TenantId - $remoteMGPath = $remoteTenantId - if ($remoteTenantId -eq $azApiCallConf['checkcontext'].tenant.id) { - $peeringXTenant = 'false' - } - else { - $peeringXTenant = 'true' - } - } - else { - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($remotesubscriptionId)?api-version=2020-01-01" - $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($remotesubscriptionId)'" - if ($remoteTenantId.id -like '/subscriptions/*') { - #sub actually could be resolved but not available in htSubscriptionsMgPath - Write-Host "SubscriptionId '$($remotesubscriptionId)' (tenantId: '$($remoteTenantId.tenantId)' (current context tenantId: '$($azapiCallConf['checkContext'].tenant.Id)')) was not captured by getSubscriptions/getEntities, however could be fully resolved with direct get call (ARM subscription API)" -ForegroundColor Magenta - $remoteMGPath = $remoteTenantId.tenantId - if ($azapiCallConf['checkContext'].tenant.Id -eq $remoteTenantId.tenantId) { - $peeringXTenant = 'false' - } - else { - $peeringXTenant = 'true' - } - } - else { - $arrayRemoteMGPath = @() - foreach ($remoteId in $remoteTenantId) { - if ($remoteId -eq 'SubscriptionNotFound Tenant unknown') { - $remoteMGPath = 'unknown' - $peeringXTenant = 'n/a' - } - else { - $objectGuid = [System.Guid]::empty - if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { - if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" - } - else { - $arrayRemoteMGPath += $remoteId - } - if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { - $peeringXTenant = 'false' - } - else { - $peeringXTenant = 'true' - } - } - $script:htUnknownTenantsForSubscription.($remotesubscriptionId) = @{} - $script:htUnknownTenantsForSubscription.($remotesubscriptionId).TenantId = $arrayRemoteMGPath -join ', ' - $remoteMGPath += $arrayRemoteMGPath -join ', ' - } - } - } - } - } - - $remotevnetName = $remotevnetIdSplit[8] - $remotevnetResourceGroup = $remotevnetIdSplit[4] - - if ($htVNets.($peering.properties.remoteVirtualNetwork.id)) { - $remotevnetState = 'existent' - $remoteLocation = $htVNets.($peering.properties.remoteVirtualNetwork.id).location - $remotePeeringsCount = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.virtualNetworkPeerings.id.Count - $remoteDhcpoptionsDnsservers = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.dhcpoptions.dnsservers - $remoteSubnetsCount = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.id.Count - $remoteSubnetsWithNSGCount = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.networkSecurityGroup.id.Count - $remoteSubnetsWithRouteTable = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.routeTable.id.Count - $remoteSubnetsWithDelegations = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.delegations.id.Count - $remotePrivateEndPoints = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.privateEndpoints.id.Count - $remoteSubnetsWithPrivateEndPoints = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.where({ $_.properties.privateEndpoints.id.Count -gt 0 }).Count - $remoteConnectedDevices = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.ipConfigurations.id.Count - $remoteSubnetsWithConnectedDevices = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.where({ $_.properties.ipConfigurations.id.Count -gt 0 }).Count - $remoteDdosProtection = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.enableDdosProtection - $remotePeering = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.virtualNetworkPeerings.where({ $_.properties.remoteVirtualNetwork.id -eq $vnet.id }) - if ($remotePeering.count -eq 1) { - $remotePeeringName = $remotePeering.name - $remotePeeringState = $remotePeering.Properties.peeringState - $remotePeeringSyncLevel = $remotePeering.Properties.peeringSyncLevel - $remoteAllowVirtualNetworkAccess = $remotePeering.properties.allowVirtualNetworkAccess - $remoteAllowForwardedTraffic = $remotePeering.properties.allowForwardedTraffic - $remoteAllowGatewayTransit = $remotePeering.properties.allowGatewayTransit - $remoteUseRemoteGateways = $remotePeering.properties.useRemoteGateways - $remoteDoNotVerifyRemoteGateways = $remotePeering.properties.doNotVerifyRemoteGateways - $remotePeerCompleteVnets = $remotePeering.properties.peerCompleteVnets - $remoteRouteServiceVips = $remotePeering.properties.routeServiceVips - } - else { - $remotePeeringName = 'n/a' - $remotePeeringState = 'n/a' - $remotePeeringSyncLevel = 'n/a' - $remoteAllowVirtualNetworkAccess = 'n/a' - $remoteAllowForwardedTraffic = 'n/a' - $remoteAllowGatewayTransit = 'n/a' - $remoteUseRemoteGateways = 'n/a' - $remoteDoNotVerifyRemoteGateways = 'n/a' - $remotePeerCompleteVnets = 'n/a' - $remoteRouteServiceVips = 'n/a' - } - - } - else { - if ($getMgParentName -eq 'Tenant Root') { - $remotevnetState = 'non-existent' - } - else { - $remotevnetState = 'n/a' - } - $remoteLocation = 'n/a' - $remotePeeringsCount = 'n/a' - $remoteDhcpoptionsDnsservers = 'n/a' - $remoteSubnetsCount = 'n/a' - $remoteSubnetsWithNSGCount = 'n/a' - $remoteSubnetsWithRouteTable = 'n/a' - $remoteSubnetsWithDelegations = 'n/a' - $remotePrivateEndPoints = 'n/a' - $remoteSubnetsWithPrivateEndPoints = 'n/a' - $remoteConnectedDevices = 'n/a' - $remoteSubnetsWithConnectedDevices = 'n/a' - $remoteDdosProtection = 'n/a' - $remotePeeringName = 'n/a' - $remotePeeringState = 'n/a' - $remotePeeringSyncLevel = 'n/a' - $remoteAllowVirtualNetworkAccess = 'n/a' - $remoteAllowForwardedTraffic = 'n/a' - $remoteAllowGatewayTransit = 'n/a' - $remoteUseRemoteGateways = 'n/a' - $remoteDoNotVerifyRemoteGateways = 'n/a' - $remotePeerCompleteVnets = 'n/a' - $remoteRouteServiceVips = 'n/a' - } - - $null = $script:arrayVirtualNetworks.Add([PSCustomObject]@{ - SubscriptionName = $subscriptionName - Subscription = ($vnet.id -split '/')[2] - MGPath = $MGPath - VNet = $vnet.name - VNetId = $vnet.id - VNetResourceGroup = $vnetResourceGroup - Location = $vnet.location - AddressSpaceAddressPrefixes = ($vnet.properties.addressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") - DhcpoptionsDnsservers = ($vnet.properties.dhcpoptions.dnsservers -join "$CsvDelimiterOpposite ") - SubnetsCount = $vnet.properties.subnets.id.Count - SubnetsWithNSGCount = $vnet.properties.subnets.properties.networkSecurityGroup.id.Count - SubnetsWithRouteTableCount = $vnet.properties.subnets.properties.routeTable.id.Count - SubnetsWithDelegationsCount = $vnet.properties.subnets.properties.delegations.id.Count - PrivateEndpointsCount = $vnet.properties.subnets.properties.privateEndpoints.id.Count - SubnetsWithPrivateEndPointsCount = $subnetsWithPrivateEndPointsCount - ConnectedDevices = $vnet.properties.subnets.properties.ipConfigurations.id.Count - SubnetsWithConnectedDevicesCount = $subnetsWithConnectedDevicesCount - DdosProtection = $vnet.properties.enableDdosProtection - - PeeringsCount = $vnet.properties.virtualNetworkPeerings.id.Count - PeeringXTenant = $peeringXTenant - PeeringName = $peering.name - PeeringState = $peering.properties.peeringState - PeeringSyncLevel = $peering.properties.peeringSyncLevel - AllowVirtualNetworkAccess = $peering.properties.allowVirtualNetworkAccess - AllowForwardedTraffic = $peering.properties.allowForwardedTraffic - AllowGatewayTransit = $peering.properties.allowGatewayTransit - UseRemoteGateways = $peering.properties.useRemoteGateways - DoNotVerifyRemoteGateways = $peering.properties.doNotVerifyRemoteGateways - PeerCompleteVnets = $peering.properties.peerCompleteVnets - RouteServiceVips = $peering.properties.routeServiceVips - - RemotePeeringsCount = $remotePeeringsCount - RemotePeeringName = $remotePeeringName - RemotePeeringState = $remotePeeringState - RemotePeeringSyncLevel = $remotePeeringSyncLevel - RemoteAllowVirtualNetworkAccess = $RemoteAllowVirtualNetworkAccess - RemoteAllowForwardedTraffic = $RemoteAllowForwardedTraffic - RemoteAllowGatewayTransit = $RemoteAllowGatewayTransit - RemoteUseRemoteGateways = $RemoteUseRemoteGateways - RemoteDoNotVerifyRemoteGateways = $RemoteDoNotVerifyRemoteGateways - RemotePeerCompleteVnets = $RemotePeerCompleteVnets - RemoteRouteServiceVips = $RemoteRouteServiceVips - - RemoteSubscriptionName = $remotesubscriptionName - RemoteSubscription = $remotesubscriptionId - RemoteMGPath = $remoteMGPath -join ', ' - RemoteVNet = $remotevnetName - RemoteVNetId = $peering.properties.remoteVirtualNetwork.id - RemoteVNetState = $remotevnetState - RemoteVNetResourceGroup = $remotevnetResourceGroup - RemoteVNetLocation = $remoteLocation - RemoteAddressSpaceAddressPrefixes = ($peering.properties.remoteAddressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") - RemoteVirtualNetworkAddressSpaceAddressPrefixes = ($peering.properties.remoteVirtualNetworkAddressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") - - RemoteDhcpoptionsDnsservers = ($remoteDhcpoptionsDnsservers -join "$CsvDelimiterOpposite ") - RemoteSubnetsCount = $remoteSubnetsCount - RemoteSubnetsWithNSGCount = $remoteSubnetsWithNSGCount - RemoteSubnetsWithRouteTable = $remoteSubnetsWithRouteTable - RemoteSubnetsWithDelegations = $remoteSubnetsWithDelegations - RemotePrivateEndPoints = $remotePrivateEndPoints - RemoteSubnetsWithPrivateEndPoints = $remoteSubnetsWithPrivateEndPoints - RemoteConnectedDevices = $remoteConnectedDevices - RemoteSubnetsWithConnectedDevices = $remoteSubnetsWithConnectedDevices - RemoteDdosProtection = $remoteDdosProtection - }) - } - - } - else { - $null = $script:arrayVirtualNetworks.Add([PSCustomObject]@{ - SubscriptionName = $subscriptionName - Subscription = ($vnet.id -split '/')[2] - MGPath = $MGPath - VNet = $vnet.name - VNetId = $vnet.id - VNetResourceGroup = $vnetResourceGroup - Location = $vnet.location - - AddressSpaceAddressPrefixes = ($vnet.properties.addressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") - DhcpoptionsDnsservers = ($vnet.properties.dhcpoptions.dnsservers -join "$CsvDelimiterOpposite ") - SubnetsCount = $vnet.properties.subnets.id.Count - SubnetsWithNSGCount = $vnet.properties.subnets.properties.networkSecurityGroup.id.Count - SubnetsWithRouteTableCount = $vnet.properties.subnets.properties.routeTable.id.Count - SubnetsWithDelegationsCount = $vnet.properties.subnets.properties.delegations.id.Count - PrivateEndpointsCount = $vnet.properties.subnets.properties.privateEndpoints.id.Count - SubnetsWithPrivateEndPointsCount = $subnetsWithPrivateEndPointsCount - ConnectedDevices = $vnet.properties.subnets.properties.ipConfigurations.id.Count - SubnetsWithConnectedDevicesCount = $subnetsWithConnectedDevicesCount - DdosProtection = $vnet.properties.enableDdosProtection - - PeeringsCount = $vnet.properties.virtualNetworkPeerings.id.Count - PeeringXTenant = 'n/a' - PeeringName = '' - PeeringState = '' - PeeringSyncLevel = '' - AllowVirtualNetworkAccess = '' - AllowForwardedTraffic = '' - AllowGatewayTransit = '' - UseRemoteGateways = '' - DoNotVerifyRemoteGateways = '' - PeerCompleteVnets = '' - RouteServiceVips = '' - - RemotePeeringsCount = '' - RemotePeeringName = '' - RemotePeeringState = '' - RemotePeeringSyncLevel = '' - RemoteAllowVirtualNetworkAccess = '' - RemoteAllowForwardedTraffic = '' - RemoteAllowGatewayTransit = '' - RemoteUseRemoteGateways = '' - RemoteDoNotVerifyRemoteGateways = '' - RemotePeerCompleteVnets = '' - RemoteRouteServiceVips = '' - - RemoteSubscriptionName = '' - RemoteSubscription = '' - RemoteMGPath = '' - RemoteVNet = '' - RemoteVNetId = '' - RemoteVNetState = '' - RemoteVNetResourceGroup = '' - RemoteVNetLocation = '' - RemoteAddressSpaceAddressPrefixes = '' - RemoteVirtualNetworkAddressSpaceAddressPrefixes = '' - RemoteDhcpoptionsDnsservers = '' - RemoteSubnetsCount = '' - RemoteSubnetsWithNSGCount = '' - RemoteSubnetsWithRouteTable = '' - RemoteSubnetsWithDelegations = '' - RemotePrivateEndPoints = '' - RemoteSubnetsWithPrivateEndPoints = '' - RemoteConnectedDevices = '' - RemoteSubnetsWithConnectedDevices = '' - RemoteDdosProtection = '' - }) - } - #endregion peerings - - #region subnets - - if ($vnet.properties.subnets.Count -gt 0) { - foreach ($subnet in $vnet.properties.subnets) { - - $script:htSubnets.($subnet.id) = @{ - SubscriptionName = $subscriptionName - Subscription = ($vnet.id -split '/')[2] - MGPath = $MGPath - VNet = $vnet.name - VNetId = $vnet.id - Location = $vnet.location - ResourceGroup = $vnetResourceGroup - } - - $arrayServiceEndPoints = @() - if ($subnet.properties.serviceEndpoints.service.Count -gt 0) { - $arrayServiceEndPoints = foreach ($serviceEndpoint in $subnet.properties.serviceEndpoints) { - "$($serviceEndpoint.service) ($(($serviceEndpoint.locations | Sort-Object) -join ', '))" - } - } - - $delegation = '' - if ($subnet.properties.delegations.Count -gt 0) { - $delegation = "$($subnet.properties.delegations.properties.serviceName) ($(($subnet.properties.delegations.properties.actions | Sort-Object) -join ', '))" - } - - #region IP address usage - #https://github.com/ElanShudnow/AzureCode/blob/242b923eada55fa795b930473a50dedf14bdc409/PowerShell/AzSubnetAvailability/AzSubnetAvailability.ps1 - # Gets the mask from the IP configuration (I.e 10.0.0.0/24, turns to just "24") - - if (-not [string]::IsNullOrWhiteSpace($subnet.properties.addressPrefix)) { - $AddressPrefix = $subnet.properties.addressPrefix - $subnetNet = $AddressPrefix -replace '/.*' - $subnetNetOutput = $subnetNet - } - - #ignore IPv6 - if (-not [string]::IsNullOrWhiteSpace($subnet.properties.addressPrefixes)) { - $arr = foreach ($entry in $subnet.properties.addressPrefixes) { - if ($entry -match '^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])\/(\d{1}|[0-2]{1}\d{1}|3[0-2])$') { - $AddressPrefix = $entry - $AddressPrefix -replace '/.*' - $subnetNet = $AddressPrefix -replace '/.*' - } - else { - "(ignoring IPv6 $entry)" - } - } - $subnetNetOutput = $arr - } - - $Mask = $AddressPrefix.substring($AddressPrefix.Length - 2, 2) - - #Amount of available IP Addresses minus the 3 IPs that Azure consumes, minus net and broadcast - #https://learn.microsoft.com/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets - switch ($Mask) { - '30' { $AvailableAddresses = [Math]::Pow(2, 2) - 5 } - '29' { $AvailableAddresses = [Math]::Pow(2, 3) - 5 } - '28' { $AvailableAddresses = [Math]::Pow(2, 4) - 5 } - '27' { $AvailableAddresses = [Math]::Pow(2, 5) - 5 } - '26' { $AvailableAddresses = [Math]::Pow(2, 6) - 5 } - '25' { $AvailableAddresses = [Math]::Pow(2, 7) - 5 } - '24' { $AvailableAddresses = [Math]::Pow(2, 8) - 5 } - '23' { $AvailableAddresses = [Math]::Pow(2, 9) - 5 } - '22' { $AvailableAddresses = [Math]::Pow(2, 10) - 5 } - '21' { $AvailableAddresses = [Math]::Pow(2, 11) - 5 } - '20' { $AvailableAddresses = [Math]::Pow(2, 12) - 5 } - '19' { $AvailableAddresses = [Math]::Pow(2, 13) - 5 } - '18' { $AvailableAddresses = [Math]::Pow(2, 14) - 5 } - '17' { $AvailableAddresses = [Math]::Pow(2, 15) - 5 } - '16' { $AvailableAddresses = [Math]::Pow(2, 16) - 5 } - '15' { $AvailableAddresses = [Math]::Pow(2, 17) - 5 } - '14' { $AvailableAddresses = [Math]::Pow(2, 18) - 5 } - '13' { $AvailableAddresses = [Math]::Pow(2, 19) - 5 } - '12' { $AvailableAddresses = [Math]::Pow(2, 20) - 5 } - '11' { $AvailableAddresses = [Math]::Pow(2, 21) - 5 } - '10' { $AvailableAddresses = [Math]::Pow(2, 22) - 5 } - '9' { $AvailableAddresses = [Math]::Pow(2, 23) - 5 } - '8' { $AvailableAddresses = [Math]::Pow(2, 24) - 5 } - } - - $IPsLeft = $AvailableAddresses - $subnet.properties.ipConfigurations.Count - $PercentIPsUsed = [math]::Round((($subnet.properties.ipConfigurations.Count / $AvailableAddresses) * 100), 1) - $subnetIPAddressUsageCritical = $false - if ($PercentIPsUsed -gt $NetworkSubnetIPAddressUsageCriticalPercentage) { - $subnetIPAddressUsageCritical = $true - } - - #endregion IP address usage - - $subnetPrefix = $AddressPrefix -replace '.*/' - - $subnetmask = ([IPAddress]"$([system.convert]::ToInt64(('1'*$subnetPrefix).PadRight(32,'0'),2))").IPAddressToString - $IPBits = [int[]]$subnetNet.Split('.') - $MaskBits = [int[]]$subnetmask.Split('.') - $NetworkIDBits = 0..3 | ForEach-Object { $IPBits[$_] -band $MaskBits[$_] } - $Broadcast = (0..3 | ForEach-Object { $NetworkIDBits[$_] + ($MaskBits[$_] -bxor 255) }) -join '.' - $Range = "$subnetNet - $Broadcast" - - $null = $script:arraySubnets.Add([PSCustomObject]@{ - SubscriptionName = $subscriptionName - Subscription = ($vnet.id -split '/')[2] - MGPath = $MGPath - VNet = $vnet.name - VNetId = $vnet.id - VNetResourceGroup = $vnetResourceGroup - Location = $vnet.location - SubnetName = $subnet.name - SubnetId = $subnet.id - SubnetNet = $subnetNetOutput -join "$CsvDelimiterOpposite " - SubnetPrefix = $subnetPrefix - Subnetmask = $subnetmask - Range = $Range - ConnectedDevices = $subnet.properties.ipConfigurations.Count - AvailableIPAddresses = $IPsLeft - UsedIPAddressesPercent = "$PercentIPsUsed %" - SubnetIPAddressUsageCritical = $subnetIPAddressUsageCritical - PrivateEndpointNetworkPolicies = $subnet.properties.privateEndpointNetworkPolicies - PrivateLinkServiceNetworkPolicies = $subnet.properties.privateLinkServiceNetworkPolicies - ServiceEndpointsCount = $subnet.properties.serviceEndpoints.service.Count - ServiceEndpoints = $arrayServiceEndPoints -join ', ' - Delegation = $delegation - NetworkSecurityGroup = $subnet.properties.networkSecurityGroup.id - RouteTable = $subnet.properties.routeTable - NatGateway = '' - PrivateEndpoints = $subnet.properties.privateEndpoints.Count - }) - } - } - #endregion subnets - } - - $end = Get-Date - Write-Host " Processing Network enrichment duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" -} -function processPrivateEndpoints { - $start = Get-Date - Write-Host 'Processing Private Endpoints enrichment' - - $script:arrayPrivateEndpointsEnriched = [System.Collections.ArrayList]@() - - if ($arrayPrivateEndPointsFromResourceProperties.Count -gt 0) { - $privateEndPointsFromResourcePropertiesToProcess = ($arrayPrivateEndPointsFromResourceProperties.where({ $arrayPrivateEndPoints.id -notcontains $_.privateEndpointConnection.Properties.privateEndpoint.id })) - $privateEndPointsFromResourcePropertiesToProcessCount = $privateEndPointsFromResourcePropertiesToProcess.Count - Write-Host " Processing Private Endpoints enrichment for $privateEndPointsFromResourcePropertiesToProcessCount Private Endpoint(s) where the Private Endpoint was not returned from the PE API endpoint but from a resource property" - if ($privateEndPointsFromResourcePropertiesToProcessCount -gt 0) { - foreach ($entry in $privateEndPointsFromResourcePropertiesToProcess) { - $peResIdSplit = $entry.privateEndpointConnection.Properties.privateEndpoint.id -split '/' - $crossSubscriptionPE = 'n/a' - $peSubscriptionId = $peResIdSplit[2] - if ($peSubscriptionId -ne $entry.ResourceSubscriptionId) { - $crossSubscriptionPE = $true - } - else { - $crossSubscriptionPE = $false - } - - $peMGPath = 'n/a' - $peXTenant = 'unknown' - if ($htSubscriptionsMgPath.($peSubscriptionId)) { - $peMGPath = $htSubscriptionsMgPath.($peSubscriptionId).pathDelimited - $peXTenant = $false - } - elseif ($htUnknownTenantsForSubscription.($peSubscriptionId)) { - $remoteTenantId = $htUnknownTenantsForSubscription.($peSubscriptionId).TenantId - $peMGPath = $remoteTenantId - if ($remoteTenantId -eq $azApiCallConf['checkcontext'].tenant.id) { - $peXTenant = $false - } - else { - $peXTenant = $true - } - } - else { - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($peSubscriptionId)?api-version=2020-01-01" - $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($peSubscriptionId)'" - $arrayRemoteMGPath = @() - foreach ($remoteId in $remoteTenantId) { - $objectGuid = [System.Guid]::empty - if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { - if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" - } - else { - $arrayRemoteMGPath += $remoteId - } - if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { - $peXTenant = $false - } - else { - $peXTenant = $true - } - } - $script:htUnknownTenantsForSubscription.($peSubscriptionId) = @{} - $script:htUnknownTenantsForSubscription.($peSubscriptionId).TenantId = $arrayRemoteMGPath -join ', ' - $peMGPath = $arrayRemoteMGPath -join ', ' - } - } - - $null = $script:arrayPrivateEndpointsEnriched.Add([PSCustomObject]@{ - PEName = $entry.privateEndpointConnection.name - PEId = $entry.privateEndpointConnection.Properties.privateEndpoint.id - PELocation = 'n/a' - PEResourceGroup = $peResIdSplit[4] - PESubscriptionName = 'n/a' - PESubscription = $peSubscriptionId - PEMGPath = $peMGPath - PEConnectionType = 'n/a' - PEConnectionState = $entry.privateEndpointConnection.Properties.privateLinkServiceConnectionState.status - CrossSubscriptionPE = $crossSubscriptionPE - CrossTenantPE = $peXTenant - - Resource = $entry.ResourceName - ResourceType = $entry.ResourceType - ResourceId = $entry.ResourceId - TargetSubresource = 'n/a' - NICName = 'n/a' - FQDN = 'n/a' - ipAddresses = 'n/a' - ResourceResourceGroup = $entry.ResourceResourceGroup - ResourceSubscriptionName = $entry.ResourceSubscriptionName - ResourceSubscriptionId = $entry.ResourceSubscriptionId - ResourceMGPath = $entry.ResourceMGPath - ResourceCrossTenant = 'false' - - Subnet = 'n/a' - SubnetId = 'n/a' - SubnetVNet = 'n/a' - SubnetVNetId = 'n/a' - SubnetVNetLocation = 'n/a' - SubnetVNetResourceGroup = 'n/a' - SubnetSubscriptionName = 'n/a' - SubnetSubscription = 'n/a' - SubnetMGPath = 'n/a' - }) - } - } - } - - Write-Host " Processing Private Endpoints enrichment for $($arrayPrivateEndPoints.Count) Private Endpoint(s) where the Private Endpoint was returned from the PE API endpoint" - $htVPrivateEndPoints = @{} - foreach ($pe in $arrayPrivateEndPoints) { - $htVPrivateEndPoints.($pe.id) = $pe - } - - $htVPrivateEndPoints = @{} - foreach ($pe in $arrayPrivateEndPoints) { - $htVPrivateEndPoints.($pe.id) = $pe - } - - foreach ($pe in $arrayPrivateEndPoints) { - - $peIdSplit = ($pe.id -split '/') - $subscriptionId = $peIdSplit[2] - $resourceGroup = $peIdSplit[4] - - $subscriptionName = 'n/a' - $MGPath = 'n/a' - if ($htSubscriptionsMgPath.($subscriptionId)) { - $subHelper = $htSubscriptionsMgPath.($subscriptionId) - $subscriptionName = $subHelper.displayName - $MGPath = $subHelper.ParentNameChainDelimited - } - - $SubnetSubscriptionName = 'n/a' - $SubnetSubscription = 'n/a' - $SubnetMGPath = 'n/a' - $SubnetVNet = 'n/a' - $SubnetVNetId = 'n/a' - $SubnetVNetLocation = 'n/a' - $SubnetVNetResourceGroup = 'n/a' - if ($htSubnets.($pe.properties.subnet.id)) { - $hlper = $htSubnets.($pe.properties.subnet.id) - $SubnetSubscriptionName = $hlper.SubscriptionName - $SubnetSubscription = $hlper.Subscription - $SubnetMGPath = $hlper.MGPath - $SubnetVNet = $hlper.VNet - $SubnetVNetId = $hlper.VNetId - $SubnetVNetLocation = $hlper.Location - $SubnetVNetResourceGroup = $hlper.ResourceGroup - } - - $resourceSplit = $false - if ($pe.properties.privateLinkServiceConnections.Count -gt 0) { - $resourceId = $pe.properties.privateLinkServiceConnections.properties.privateLinkServiceId - $targetSubresource = $pe.properties.privateLinkServiceConnections.properties.groupIds -join ', ' - $resourceSplit = $pe.properties.privateLinkServiceConnections.properties.privateLinkServiceId -split '/' - $peConnectionType = 'direct' - $peConnectionState = $pe.properties.privateLinkServiceConnections.properties.privateLinkServiceConnectionState.status - } - if ($pe.properties.manualPrivateLinkServiceConnections.Count -gt 0) { - $resourceId = $pe.properties.manualPrivateLinkServiceConnections.properties.privateLinkServiceId - $targetSubresource = $pe.properties.manualPrivateLinkServiceConnections.properties.groupIds -join ', ' - $resourceSplit = $pe.properties.manualPrivateLinkServiceConnections.properties.privateLinkServiceId -split '/' - $peConnectionType = 'manual' - $peConnectionState = $pe.properties.manualPrivateLinkServiceConnections.properties.privateLinkServiceConnectionState.status - } - - $resourceSubscriptionId = 'n/a' - $resource = 'n/a' - $resourceType = 'n/a' - $resourceResourceGroup = 'n/a' - $resourceSubscriptionName = 'n/a' - $resourceMGPath = 'n/a' - $crossSubscriptionPE = 'n/a' - $resourceXTenant = 'unknown' - - if ($resourceSplit) { - $ObjectGuid = [System.Guid]::empty - if ([System.Guid]::TryParse($resourceSplit[2], [System.Management.Automation.PSReference]$ObjectGuid)) { - $resourceSubscriptionId = $resourceSplit[2] - $resource = $resourceSplit[8] - $resourceType = "$($resourceSplit[6])/$($resourceSplit[7])" - $resourceResourceGroup = $resourceSplit[4] - - if ($htSubscriptionsMgPath.($resourceSubscriptionId)) { - $subHelper = $htSubscriptionsMgPath.($resourceSubscriptionId) - $resourceSubscriptionName = $subHelper.displayName - $resourceMGPath = $subHelper.ParentNameChainDelimited - $resourceXTenant = $false - } - else { - if ($htUnknownTenantsForSubscription.($resourceSubscriptionId)) { - $remoteTenantId = $htUnknownTenantsForSubscription.($resourceSubscriptionId).TenantId - $resourceMGPath = $remoteTenantId - if ($remoteTenantId -eq $azApiCallConf['checkcontext'].tenant.id) { - $resourceXTenant = $false - } - else { - $resourceXTenant = $true - } - } - else { - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($resourceSubscriptionId)?api-version=2020-01-01" - $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($resourceSubscriptionId)'" - $arrayRemoteMGPath = @() - foreach ($remoteId in $remoteTenantId) { - $objectGuid = [System.Guid]::empty - if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { - if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" - } - else { - $arrayRemoteMGPath += $remoteId - } - if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { - $resourceXTenant = $false - } - else { - $resourceXTenant = $true - } - } - $script:htUnknownTenantsForSubscription.($resourceSubscriptionId) = @{} - $script:htUnknownTenantsForSubscription.($resourceSubscriptionId).TenantId = $arrayRemoteMGPath -join ', ' - $resourceMGPath = $arrayRemoteMGPath -join ', ' - } - } - } - - if ($SubnetSubscription -eq $resourceSubscriptionId) { - $crossSubscriptionPE = $false - } - else { - $crossSubscriptionPE = $true - } - - $crossTenantPE = $false - if ($resourceXTenant -eq $true) { - $crossTenantPE = $true - } - - } - } - - $null = $script:arrayPrivateEndpointsEnriched.Add([PSCustomObject]@{ - PEName = $pe.name - PEId = $pe.id - PELocation = $pe.location - PEResourceGroup = $resourceGroup - PESubscriptionName = $subscriptionName - PESubscription = ($pe.id -split '/')[2] - PEMGPath = $MGPath - PEConnectionType = $peConnectionType - PEConnectionState = $peConnectionState - CrossSubscriptionPE = $crossSubscriptionPE - CrossTenantPE = $crossTenantPE - - Resource = $resource - ResourceType = $resourceType - ResourceId = $resourceId - TargetSubresource = $targetSubresource -join ', ' - NICName = $pe.properties.customNetworkInterfaceName - FQDN = $pe.properties.customDnsConfigs.fqdn -join ', ' - ipAddresses = $pe.properties.customDnsConfigs.ipAddresses -join ', ' - ResourceResourceGroup = $resourceResourceGroup - ResourceSubscriptionName = $resourceSubscriptionName - ResourceSubscriptionId = $resourceSubscriptionId - ResourceMGPath = $resourceMGPath - ResourceCrossTenant = $resourceXTenant - - Subnet = $pe.properties.subnet.id -replace '.*/' - SubnetId = $pe.properties.subnet.id - SubnetVNet = $SubnetVNet - SubnetVNetId = $SubnetVNetId - SubnetVNetLocation = $SubnetVNetLocation - SubnetVNetResourceGroup = $SubnetVNetResourceGroup - SubnetSubscriptionName = $SubnetSubscriptionName - SubnetSubscription = $SubnetSubscription - SubnetMGPath = $SubnetMGPath - }) - } - - - $end = Get-Date - Write-Host " Processing Private Endpoints enrichment duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" -} -function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subscriptionsMgId) { - $script:scopescnter++ - $htmlScopeInsights = $null - $htmlScopeInsights = [System.Text.StringBuilder]::new() - #region ScopeInsightsBaseCollection - if ($mgOrSub -eq 'mg') { - #$startScopeInsightsPreQueryMg = Get-Date - #BLUEPRINT - $blueprintReleatedQuery = $blueprintBaseQuery.where( { $_.MgId -eq $mgChild -and [String]::IsNullOrEmpty($_.SubscriptionId) -and [String]::IsNullOrEmpty($_.BlueprintAssignmentId) } ) - $blueprintsScoped = $blueprintReleatedQuery - $blueprintsScopedCount = ($blueprintsScoped).count - #Resources - $mgAllChildSubscriptions = [System.Collections.ArrayList]@() - $mgAllChildSubscriptions = foreach ($entry in $htSubscriptionsMgPath.keys) { - if (($htSubscriptionsMgPath.($entry).ParentNameChain) -contains $mgchild) { - $entry - } - } - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - $resourcesAllChildSubscriptions = [System.Collections.ArrayList]@() - foreach ($mgAllChildSubscription in $mgAllChildSubscriptions) { - foreach ($resource in ($resourcesAllGroupedBySubcriptionId.where( { $_.name -eq $mgAllChildSubscription } )).group | Sort-Object -Property type, location) { - $null = $resourcesAllChildSubscriptions.Add($resource) - } - - } - $resourcesAllChildSubscriptionsArray = [System.Collections.ArrayList]@() - $grp = $resourcesAllChildSubscriptions | Group-Object -Property type, location - foreach ($resLoc in $grp) { - $cnt = 0 - $ResoureTypeAndLocation = $resLoc.Name.split(',') - $resLoc.Group.count_ | ForEach-Object { $cnt += $_ } - $null = $resourcesAllChildSubscriptionsArray.Add([PSCustomObject]@{ - ResourceType = $ResoureTypeAndLocation[0] - Location = $ResoureTypeAndLocation[1] - ResourceCount = $cnt - }) - } - $resourcesAllChildSubscriptions.count_ | ForEach-Object { $resourcesAllChildSubscriptionTotal += $_ } - $resourcesAllChildSubscriptionResourceTypeCount = (($resourcesAllChildSubscriptions | Sort-Object -Property type -Unique) | Measure-Object).count - $resourcesAllChildSubscriptionLocationCount = (($resourcesAllChildSubscriptions | Sort-Object -Property location -Unique) | Measure-Object).count - } - #childrenMgInfo - $mgAllChildMgs = [System.Collections.ArrayList]@() - $mgAllChildMgs = foreach ($entry in $htManagementGroupsMgPath.keys) { - if (($htManagementGroupsMgPath.($entry).path) -contains $mgchild) { - $entry - } - } - - $arrayPolicyAssignmentsEnrichedForThisManagementGroup = ($arrayPolicyAssignmentsEnrichedGroupedByManagementGroup.where( { $_.name -eq $mgChild } )).group - $arrayPolicyAssignmentsEnrichedForThisManagementGroupGroupedByPolicyVariant = $arrayPolicyAssignmentsEnrichedForThisManagementGroup | Group-Object -Property PolicyVariant - $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicy = ($arrayPolicyAssignmentsEnrichedForThisManagementGroupGroupedByPolicyVariant.where( { $_.name -eq 'Policy' } )).group - $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicySet = ($arrayPolicyAssignmentsEnrichedForThisManagementGroupGroupedByPolicyVariant.where( { $_.name -eq 'PolicySet' } )).group - - if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { - if ([string]::IsNullOrEmpty(($htMgASCSecureScore).($mgChild).SecureScore) -or [string]::IsNullOrWhiteSpace(($htMgASCSecureScore).($mgChild).SecureScore)) { - $managementGroupASCPoints = 'n/a' - } - else { - $managementGroupASCPoints = ($htMgASCSecureScore).($mgChild).SecureScore - } - } - else { - $managementGroupASCPoints = "excluded (-NoMDfCSecureScore $($azAPICallConf['htParameters'].NoMDfCSecureScore))" - } - - $cssClass = 'mgDetailsTable' - - #$endScopeInsightsPreQueryMg = Get-Date - #Write-Host " ScopeInsights MG PreQuery processing duration: $((NEW-TIMESPAN -Start $startScopeInsightsPreQueryMg -End $endScopeInsightsPreQueryMg).TotalSeconds) seconds" - } - if ($mgOrSub -eq 'sub') { - #$startScopeInsightsPreQuerySub = Get-Date - #BLUEPRINT - $blueprintReleatedQuery = $blueprintBaseQuery.where( { $_.SubscriptionId -eq $subscriptionId -and -not [String]::IsNullOrEmpty($_.BlueprintName) } ) - $blueprintsAssigned = $blueprintReleatedQuery.where( { -not [String]::IsNullOrEmpty($_.BlueprintAssignmentId) } ) - $blueprintsAssignedCount = ($blueprintsAssigned).count - $blueprintsScoped = $blueprintReleatedQuery.where( { $_.BlueprintScoped -eq "/subscriptions/$subscriptionId" -and [String]::IsNullOrEmpty($_.BlueprintAssignmentId) } ) - $blueprintsScopedCount = ($blueprintsScoped).count - #SubscriptionDetails - $subPath = $htSubscriptionsMgPath.($subscriptionId).pathDelimited - $subscriptionDetailsReleatedQuery = $htSubDetails.($subscriptionId).details - $subscriptionState = ($subscriptionDetailsReleatedQuery).SubscriptionState - $subscriptionQuotaId = ($subscriptionDetailsReleatedQuery).SubscriptionQuotaId - $subscriptionResourceGroupsCount = ($resourceGroupsAll.where( { $_.subscriptionId -eq $subscriptionId } )).count_ - if (-not $subscriptionResourceGroupsCount) { - $subscriptionResourceGroupsCount = 0 - } - $subscriptionASCPoints = ($subscriptionDetailsReleatedQuery).SubscriptionASCSecureScore - - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - #Resources - $resourcesSubscription = [System.Collections.ArrayList]@() - foreach ($resource in ($resourcesAllGroupedBySubcriptionId.where( { $_.name -eq $subscriptionId } )).group | Sort-Object -Property type, location) { - $null = $resourcesSubscription.Add($resource) - } - - $resourcesSubscriptionTotal = 0 - $resourcesSubscription.count_ | ForEach-Object { $resourcesSubscriptionTotal += $_ } - $resourcesSubscriptionResourceTypeCount = (($resourcesSubscription | Sort-Object -Property type -Unique)).count - $resourcesSubscriptionLocationCount = (($resourcesSubscription | Sort-Object -Property location -Unique)).count - } - - $arrayPolicyAssignmentsEnrichedForThisSubscription = ($arrayPolicyAssignmentsEnrichedGroupedBySubscription.where( { $_.name -eq $subscriptionId } )).group - $arrayPolicyAssignmentsEnrichedForThisSubscriptionGroupedByPolicyVariant = $arrayPolicyAssignmentsEnrichedForThisSubscription | Group-Object -Property PolicyVariant - $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicy = ($arrayPolicyAssignmentsEnrichedForThisSubscriptionGroupedByPolicyVariant.where( { $_.name -eq 'Policy' } )).group - $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicySet = ($arrayPolicyAssignmentsEnrichedForThisSubscriptionGroupedByPolicyVariant.where( { $_.name -eq 'PolicySet' } )).group - - $arrayDefenderPlansSubscription = $defenderPlansGroupedBySub.where( { $_.Name -like "*$($subscriptionId)*" } ) - - $arrayUserAssignedIdentities4ResourcesSubscription = $arrayUserAssignedIdentities4Resources.where( { $_.resourceSubscriptionId -eq $subscriptionId -or $_.miSubscriptionId -eq $subscriptionId } ) - $arrayUserAssignedIdentities4ResourcesSubscriptionCount = $arrayUserAssignedIdentities4ResourcesSubscription.Count - - if ($subFeaturesGroupedBySubscription) { - $subscriptionFeatures = $subFeaturesGroupedBySubscription.where({ $_.name -eq $subscriptionId }) - } - - $cssClass = 'subDetailsTable' - - #$endScopeInsightsPreQuerySub = Get-Date - #Write-Host " ScopeInsights SUB PreQuery processing duration: $((NEW-TIMESPAN -Start $startScopeInsightsPreQuerySub -End $endScopeInsightsPreQuerySub).TotalSeconds) seconds" - } - #endregion ScopeInsightsBaseCollection - - if ($mgOrSub -eq 'sub') { - - if ($htDefenderEmailContacts.($subscriptionDetailsReleatedQuery.subscriptionId)) { - $hlpDefenderEmailContacts = $htDefenderEmailContacts.($subscriptionDetailsReleatedQuery.subscriptionId) - $MDfCEmailNotificationsState = $hlpDefenderEmailContacts.alertNotificationsState - $MDfCEmailNotificationsSeverity = $hlpDefenderEmailContacts.alertNotificationsminimalSeverity - $MDfCEmailNotificationsRoles = $hlpDefenderEmailContacts.roles - $MDfCEmailNotificationsEmails = $hlpDefenderEmailContacts.emails - } - else { - $MDfCEmailNotificationsState = '' - $MDfCEmailNotificationsSeverity = '' - $MDfCEmailNotificationsRoles = '' - $MDfCEmailNotificationsEmails = '' - } - - [void]$htmlScopeInsights.AppendLine(@" -
Subscription Name: $($subscriptionDetailsReleatedQuery.subscription -replace '<', '<' -replace '>', '>')
Subscription Id: $($subscriptionDetailsReleatedQuery.subscriptionId)
Subscription Path: $subPath
State: $subscriptionState
QuotaId: $subscriptionQuotaId
Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState
Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity
Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles
Microsoft Defender for Cloud 'Email notifications' emails: $MDfCEmailNotificationsEmails
-"@) - - #region ScopeInsightsDefenderPlans - if ($arrayDefenderPlansSubscription) { - - $defenderPlanSubscriptionDeprecatedContainerRegistry = $false - $defenderPlanSubscriptionDeprecatedKubernetesService = $false - - $containerRegistryStandardCount = ($arrayDefenderPlansSubscription.Group.where( { $_.defenderPlan -eq 'ContainerRegistry' -and $_.defenderPlanTier -eq 'Standard' } )).Count - $kubernetesServiceStandardCount = ($arrayDefenderPlansSubscription.Group.where( { $_.defenderPlan -eq 'KubernetesService' -and $_.defenderPlanTier -eq 'Standard' } )).Count - if ($containerRegistryStandardCount -gt 0) { - $defenderPlanSubscriptionDeprecatedContainerRegistry = $true - } - if ($kubernetesServiceStandardCount -gt 0) { - $defenderPlanSubscriptionDeprecatedKubernetesService = $true - } - - $defenderCapabilitiesSubscription = ($arrayDefenderPlansSubscription.group.defenderPlan | Sort-Object -Unique) - $tfCount = 1 - $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-"@) - - if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { - [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
-'@) - } - if ($defenderPlanSubscriptionDeprecatedKubernetesService) { - [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
-'@) - } - - [void]$htmlScopeInsights.AppendLine(@" -   Download CSV semicolon | comma - - - - - - - - - -"@) - - foreach ($plan in $arrayDefenderPlansSubscription.Group | Sort-Object -Property defenderPlan) { - if (($plan.defenderPlan -eq 'ContainerRegistry' -and $plan.defenderPlanTier -eq 'Standard') -or ($plan.defenderPlan -eq 'KubernetesService' -and $plan.defenderPlanTier -eq 'Standard')) { - $thisDefenderPlan = " $($plan.defenderPlan)" - } - else { - $thisDefenderPlan = $plan.defenderPlan - } - [void]$htmlScopeInsights.AppendLine(@" - - - - -"@) - } - [void]$htmlScopeInsights.AppendLine(@" - - -
PlanTier
$($thisDefenderPlan)$($plan.defenderPlanTier)
- -
-"@) - } - else { - $subscriptionSkippedMDfC = $arrayDefenderPlansSubscriptionsSkipped.where( { $_.subscriptionId -eq $subscriptionId } ) - if ($subscriptionSkippedMDfC.Count -gt 0) { - if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { - [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) -"@) - } - - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs -'@) - } - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsDefenderPlans - - #region ScopeInsightsDiganosticsSubscription - if (($htDiagnosticSettingsMgSub).sub.($subscriptionId)) { - $diagnosticsSubCount = (($htDiagnosticSettingsMgSub).sub.($subscriptionId).Values.Count) - $tfCount = $diagnosticsSubCount - $htmlTableId = "ScopeInsights_DiagnosticsSub_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - -"@) - foreach ($logCategory in $diagnosticSettingsSubCategories) { - [void]$htmlScopeInsights.AppendLine(@" - -"@) - } - [void]$htmlScopeInsights.AppendLine(@' - - - -'@) - $htmlScopeInsightsDiagnosticsSub = $null - $htmlScopeInsightsDiagnosticsSub = foreach ($entry in ($htDiagnosticSettingsMgSub).sub.($subscriptionId).keys | Sort-Object) { - foreach ($diagset in ($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.keys | Sort-Object) { - @" - - - - -"@ - foreach ($logCategory in $diagnosticSettingsSubCategories) { - if (($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticCategoriesHt.($logCategory)) { - @" - -"@ - } - else { - @' - -'@ - } - } - @' - -'@ - } - } - - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsSub) - [void]$htmlScopeInsights.AppendLine(@" - -
Diagnostic settingTargetTarget Id$logCategory
$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticSettingName)$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticTargetType)$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticTargetId)$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticCategoriesHt.($logCategory))n/a
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsDiganosticsSubscription - - #Tags - #region ScopeInsightsTags - $tagsSubscriptionCount = ($htSubscriptionTags.$subscriptionId.Keys).count - if ($tagsSubscriptionCount -gt 0) { - $tfCount = $tagsSubscriptionCount - $htmlTableId = "ScopeInsights_Tags_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - -"@) - $htmlScopeInsightsTags = $null - $htmlScopeInsightsTags = foreach ($tag in (($htSubscriptionTags).($subscriptionId)).keys | Sort-Object) { - @" - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsTags) - [void]$htmlScopeInsights.AppendLine(@" - -
Tag NameTag Value
$tag$($htSubscriptionTags.$subscriptionId[$tag])
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $tagsSubscriptionCount Subscription Tags -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsTags - - #TagNameUsage - #region ScopeInsightsTagNameUsage - $arrayTagListSubscription = [System.Collections.ArrayList]@() - foreach ($tagScope in $htSubscriptionTagList.($subscriptionId).keys) { - foreach ($tagScopeTagName in $htSubscriptionTagList.($subscriptionId).$tagScope.keys) { - $null = $arrayTagListSubscription.Add([PSCustomObject]@{ - Scope = $tagScope - TagName = ($tagScopeTagName) - TagCount = $htSubscriptionTagList.($subscriptionId).($tagScope).($tagScopeTagName) - }) - } - } - $tagsUsageCount = ($arrayTagListSubscription).Count - - if ($tagsUsageCount -gt 0) { - $tagNamesUniqueCount = ($arrayTagListSubscription | Sort-Object -Property TagName -Unique).Count - $tagNamesUsedInScopes = ($arrayTagListSubscription | Sort-Object -Property Scope -Unique).scope -join "$($CsvDelimiterOpposite) " - $tfCount = $tagsUsageCount - $htmlTableId = "ScopeInsights_TagNameUsage_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Resource naming and tagging decision guide docs
-   Download CSV semicolon | comma - - - - - - - - - -"@) - $htmlScopeInsightsTagsUsage = $null - $htmlScopeInsightsTagsUsage = foreach ($tagEntry in $arrayTagListSubscription | Sort-Object Scope, TagName -CaseSensitive) { - @" - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsTagsUsage) - [void]$htmlScopeInsights.AppendLine(@" - -
ScopeTagNameCount
$($tagEntry.Scope)$($tagEntry.TagName)$($tagEntry.TagCount)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsTagNameUsage - - #Consumption - #$startScopeInsightsConsumptionSub = Get-Date - #region ScopeInsightsConsumptionSub - if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { - - if ($htAzureConsumptionSubscriptions.($subscriptionId).ConsumptionData) { - $consumptionData = $htAzureConsumptionSubscriptions.($subscriptionId).ConsumptionData - - $arrayTotalCostSummarySub = @() - $arrayConsumptionData = [System.Collections.ArrayList]@() - - $totalCost = 0 - - $currency = $htAzureConsumptionSubscriptions.($subscriptionId).Currency - $consumedServiceCount = ($consumptionData.ResourceType | Sort-Object -Unique | Measure-Object).Count - $resourceCount = ($consumptionData.ResourceId | Sort-Object -Unique | Measure-Object).Count - $subConsumptionDataGrouped = $consumptionData | Group-Object -Property ResourceType, ChargeType, MeterCategory - - foreach ($consumptionline in $subConsumptionDataGrouped) { - - $costConsumptionLine = ($consumptionline.group.PreTaxCost | Measure-Object -Sum).Sum - if ([math]::Round($costConsumptionLine, 2) -eq 0) { - $cost = $costConsumptionLine.ToString('0.0000') - } - else { - $cost = [math]::Round($costConsumptionLine, 2).ToString('0.00') - } - - $null = $arrayConsumptionData.Add([PSCustomObject]@{ - ResourceType = ($consumptionline.name).split(', ')[0] - ConsumedServiceChargeType = ($consumptionline.name).split(', ')[1] - ConsumedServiceCategory = ($consumptionline.name).split(', ')[2] - ConsumedServiceInstanceCount = $consumptionline.Count - ConsumedServiceCost = $cost #[decimal]$cost - ConsumedServiceCurrency = $currency - }) - - $totalCost = $htAzureConsumptionSubscriptions.($subscriptionId).TotalCost - - } - if ([math]::Round($totalCost, 2) -eq 0) { - $totalCost = $totalCost - } - else { - $totalCost = [math]::Round($totalCost, 2).ToString('0.00') - } - $arrayTotalCostSummarySub += "$($totalCost) $($currency) generated by $($resourceCount) Resources ($($consumedServiceCount) ResourceTypes)" - - $tfCount = ($arrayConsumptionData | Measure-Object).Count - $htmlTableId = "ScopeInsights_Consumption_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - - - -"@) - $htmlScopeInsightsConsumptionSub = $null - $htmlScopeInsightsConsumptionSub = foreach ($consumptionLine in $arrayConsumptionData) { - @" - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsConsumptionSub) - [void]$htmlScopeInsights.AppendLine(@" - -
ChargeTypeResourceTypeCategoryResourceCountCost ($($AzureConsumptionPeriod)d)Currency
$($consumptionLine.ConsumedServiceChargeType)$($consumptionLine.ResourceType)$($consumptionLine.ConsumedServiceCategory)$($consumptionLine.ConsumedServiceInstanceCount)$($consumptionLine.ConsumedServiceCost)$($currency)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Consumption data available -'@) - } - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Consumption data available as switch parameter -DoAzureConsumption was not applied -'@) - } - - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsConsumptionSub - #$endScopeInsightsConsumptionSub = Get-Date - #Write-Host " **ScopeInsightsConsumptionSub data duration: $((NEW-TIMESPAN -Start $startScopeInsightsConsumptionSub -End $endScopeInsightsConsumptionSub).TotalSeconds) seconds" - - #ResourceGroups - #region ScopeInsightsResourceGroups - if ($subscriptionResourceGroupsCount -gt 0) { - [void]$htmlScopeInsights.AppendLine(@" - $subscriptionResourceGroupsCount Resource Groups | Limit: ($subscriptionResourceGroupsCount/$LimitResourceGroups) -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $subscriptionResourceGroupsCount Resource Groups -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsResourceGroups - - #ResourceProvider - #region ScopeInsightsResourceProvidersDetailed - if ($azAPICallConf['htParameters'].NoResourceProvidersAtAll -eq $false) { - if ($azAPICallConf['htParameters'].NoResourceProvidersDetailed -eq $false) { - if (($htResourceProvidersAll).($subscriptionId)) { - $tfCount = ($htResourceProvidersAll).($subscriptionId).Providers.Count - $htmlTableId = "ScopeInsights_ResourceProvider_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - -"@) - $htmlScopeInsightsResourceProvidersDetailed = $null - $htmlScopeInsightsResourceProvidersDetailed = foreach ($provider in ($htResourceProvidersAll).($subscriptionId).Providers) { - @" - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsResourceProvidersDetailed) - [void]$htmlScopeInsights.AppendLine(@" - -
ProviderState
$($provider.namespace)$($provider.registrationState)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $(($htResourceProvidersAll.Keys).count) Resource Providers -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - } - #endregion ScopeInsightsResourceProvidersDetailed - - #region ScopeInsightsSubscriptionFeatures - if ($subscriptionFeatures) { - $subscriptionFeaturesCount = $subscriptionFeatures.Group.Count - - $tfCount = $subscriptionFeaturesCount - $htmlTableId = "ScopeInsights_SubscriptionFeatures_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - - [void]$htmlScopeInsights.AppendLine(@" - -
-   Set up preview features in Azure subscription docs - - - - - - - -"@) - - foreach ($feature in $subscriptionFeatures.Group | Sort-Object -Property feature) { - [void]$htmlScopeInsights.AppendLine(@" - -"@) - } - - - [void]$htmlScopeInsights.AppendLine(@" - -
Feature
$($feature.feature)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsSubscriptionFeatures - - #ResourceLocks - #region ScopeInsightsResourceLocks - if ($htResourceLocks.($subscriptionId)) { - $tfCount = 6 - $htmlTableId = "ScopeInsights_ResourceLocks_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - - $subscriptionLocksCannotDeleteCount = $htResourceLocks.($subscriptionId).SubscriptionLocksCannotDeleteCount - $subscriptionLocksReadOnlyCount = $htResourceLocks.($subscriptionId).SubscriptionLocksReadOnlyCount - $resourceGroupsLocksCannotDeleteCount = $htResourceLocks.($subscriptionId).ResourceGroupsLocksCannotDeleteCount - $resourceGroupsLocksReadOnlyCount = $htResourceLocks.($subscriptionId).ResourceGroupsLocksReadOnlyCount - $resourcesLocksCannotDeleteCount = $htResourceLocks.($subscriptionId).ResourcesLocksCannotDeleteCount - $resourcesLocksReadOnlyCount = $htResourceLocks.($subscriptionId).ResourcesLocksReadOnlyCount - - [void]$htmlScopeInsights.AppendLine(@" - -
-   Considerations before applying locks docs - - - - - - - - - - - - - - - - -
Lock scopeLock typepresence
SubscriptionCannotDelete$($subscriptionLocksCannotDeleteCount)
SubscriptionReadOnly$($subscriptionLocksReadOnlyCount)
ResourceGroupCannotDelete$($resourceGroupsLocksCannotDeleteCount)
ResourceGroupReadOnly$($resourceGroupsLocksReadOnlyCount)
ResourceCannotDelete$($resourcesLocksCannotDeleteCount)
ResourceReadOnly$($resourcesLocksReadOnlyCount)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsResourceLocks - - } - - #MgChildInfo - #region ScopeInsightsManagementGroups - if ($mgOrSub -eq 'mg') { - - [void]$htmlScopeInsights.AppendLine(@" -
$(($mgAllChildMgs).count -1) ManagementGroups below this scope
$(($mgAllChildSubscriptions).count) Subscriptions below this scope
Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
-"@) - - #region ScopeInsightsDiagnosticsMg - if (($htDiagnosticSettingsMgSub).mg.($mgChild)) { - $diagnosticsMgCount = (($htDiagnosticSettingsMgSub).mg.($mgChild).Values.Count) - $tfCount = $diagnosticsMgCount - $htmlTableId = "ScopeInsights_DiagnosticsMg_$($mgChild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - -"@) - foreach ($logCategory in $diagnosticSettingsMgCategories) { - [void]$htmlScopeInsights.AppendLine(@" - -"@) - } - [void]$htmlScopeInsights.AppendLine(@' - - - -'@) - $htmlScopeInsightsDiagnosticsMg = $null - $htmlScopeInsightsDiagnosticsMg = foreach ($entry in ($htDiagnosticSettingsMgSub).mg.($mgChild).keys | Sort-Object) { - foreach ($diagset in ($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.keys | Sort-Object) { - @" - - - - -"@ - foreach ($logCategory in $diagnosticSettingsMgCategories) { - if (($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticCategoriesHt.($logCategory)) { - @" - -"@ - } - else { - @' - -'@ - } - } - @' - -'@ - } - } - - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsMg) - [void]$htmlScopeInsights.AppendLine(@" - -
Diagnostic settingTargetTarget Id$logCategory
$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticSettingName)$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticTargetType)$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticTargetId)$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticCategoriesHt.($logCategory))n/a
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs -'@) - } - #endregion ScopeInsightsDiagnosticsMg - - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - - #$startScopeInsightsConsumptionMg = Get-Date - #region ScopeInsightsConsumptionMg - if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { - if ($allConsumptionDataCount -gt 0) { - - $consumptionData = $htManagementGroupsCost.($mgchild).consumptionDataSubscriptions - if (($consumptionData | Measure-Object).Count -gt 0) { - $arrayTotalCostSummaryMg = @() - $arrayConsumptionData = [System.Collections.ArrayList]@() - $consumptionDataGroupedByCurrency = $consumptionData | Group-Object -Property Currency - foreach ($currency in $consumptionDataGroupedByCurrency) { - $totalCost = 0 - $tenantSummaryConsumptionDataGrouped = $currency.group | Group-Object -Property ResourceType, ChargeType, MeterCategory - $subsCount = ($tenantSummaryConsumptionDataGrouped.group.subscriptionId | Sort-Object -Unique).Count - $consumedServiceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceType | Sort-Object -Unique).Count - $resourceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceId | Sort-Object -Unique).Count - foreach ($consumptionline in $tenantSummaryConsumptionDataGrouped) { - - $costConsumptionLine = ($consumptionline.group.PreTaxCost | Measure-Object -Sum).Sum - if ([math]::Round($costConsumptionLine, 2) -eq 0) { - $cost = $costConsumptionLine.ToString('0.0000') - } - else { - $cost = [math]::Round($costConsumptionLine, 2).ToString('0.00') - } - - $null = $arrayConsumptionData.Add([PSCustomObject]@{ - ResourceType = ($consumptionline.name).split(', ')[0] - ConsumedServiceChargeType = ($consumptionline.name).split(', ')[1] - ConsumedServiceCategory = ($consumptionline.name).split(', ')[2] - ConsumedServiceInstanceCount = $consumptionline.Count - ConsumedServiceCost = $cost #[decimal]$cost - ConsumedServiceSubscriptions = ($consumptionline.group.SubscriptionId | Sort-Object -Unique).Count - ConsumedServiceCurrency = $currency.Name - }) - - $totalCost = $totalCost + $costConsumptionLine - } - if ([math]::Round($totalCost, 2) -eq 0) { - $totalCost = $totalCost.ToString('0.0000') - } - else { - $totalCost = [math]::Round($totalCost, 2).ToString('0.00') - } - $arrayTotalCostSummaryMg += "$($totalCost) $($currency.Name) generated by $($resourceCount) Resources ($($consumedServiceCount) ResourceTypes) in $($subsCount) Subscriptions" - } - - $tfCount = ($arrayConsumptionData).Count - $htmlTableId = "ScopeInsights_Consumption_$($mgChild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV -semicolon | -comma - - - - - - - - - - - - - -"@) - $htmlScopeInsightsConsumptionMg = $null - $htmlScopeInsightsConsumptionMg = foreach ($consumptionLine in $arrayConsumptionData) { - @" - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsConsumptionMg) - [void]$htmlScopeInsights.AppendLine(@" - -
ChargeTypeResourceTypeCategoryResourceCountCost ($($AzureConsumptionPeriod)d)CurrencySubscriptions
$($consumptionLine.ConsumedServiceChargeType)$($consumptionLine.ResourceType)$($consumptionLine.ConsumedServiceCategory)$($consumptionLine.ConsumedServiceInstanceCount)$($consumptionLine.ConsumedServiceCost)$($consumptionLine.ConsumedServiceCurrency)$($consumptionLine.ConsumedServiceSubscriptions)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Consumption data available for Subscriptions under this ManagementGroup -'@) - } - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Consumption data available -'@) - } - - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Consumption data available as switch parameter -DoAzureConsumption was not applied -'@) - } - #endregion ScopeInsightsConsumptionMg - #$endScopeInsightsConsumptionMg = Get-Date - #Write-Host " ++ScopeInsightsConsumptionMg duration: ($((NEW-TIMESPAN -Start $startScopeInsightsConsumptionMg -End $endScopeInsightsConsumptionMg).TotalSeconds) seconds)" - - - } - #endregion ScopeInsightsManagementGroups - - #ScopeInsightsResources - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - #resources - #region ScopeInsightsResources - if ($mgOrSub -eq 'mg') { - if ($resourcesAllChildSubscriptionLocationCount -gt 0) { - $tfCount = ($resourcesAllChildSubscriptionsArray).count - $htmlTableId = "ScopeInsights_Resources_$($mgChild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - -"@) - $htmlScopeInsightsResources = $null - $htmlScopeInsightsResources = foreach ($resourceAllChildSubscriptionResourceTypePerLocation in $resourcesAllChildSubscriptionsArray | Sort-Object @{Expression = { $_.ResourceType } }, @{Expression = { $_.location } }) { - @" - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsResources) - [void]$htmlScopeInsights.AppendLine(@" - -
ResourceTypeLocationCount
$($resourceAllChildSubscriptionResourceTypePerLocation.ResourceType)$($resourceAllChildSubscriptionResourceTypePerLocation.location)$($resourceAllChildSubscriptionResourceTypePerLocation.ResourceCount)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $resourcesAllChildSubscriptionResourceTypeCount ResourceTypes (all Subscriptions below this scope) -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - - if ($mgOrSub -eq 'sub') { - if ($resourcesSubscriptionResourceTypeCount -gt 0) { - $tfCount = ($resourcesSubscription).Count - $htmlTableId = "ScopeInsights_Resources_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - -"@) - $htmlScopeInsightsResources = $null - $htmlScopeInsightsResources = foreach ($resourceSubscriptionResourceTypePerLocation in $resourcesSubscription | Sort-Object @{Expression = { $_.type } }, @{Expression = { $_.location } }, @{Expression = { $_.count_ } }) { - @" - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsResources) - [void]$htmlScopeInsights.AppendLine(@" - -
ResourceTypeLocationCount
$($resourceSubscriptionResourceTypePerLocation.type)$($resourceSubscriptionResourceTypePerLocation.location)$($resourceSubscriptionResourceTypePerLocation.count_)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $resourcesSubscriptionResourceTypeCount ResourceTypes -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - #endregion ScopeInsightsResources - } - - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - #region ScopeInsightsCAFResourceNamingALL - if ($mgOrSub -eq 'sub') { - $resourcesIdsAllCAFNamingRelevantThisSubscription = $resourcesIdsAllCAFNamingRelevantGroupedBySubscription.where({ $_.Name -eq $subscriptionId }) - if ($resourcesIdsAllCAFNamingRelevantThisSubscription) { - $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByType = $resourcesIdsAllCAFNamingRelevantThisSubscription.Group | Group-Object -Property type - $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByTypeCount = ($resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByType | Measure-Object).Count - - $tfCount = $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByTypeCount - $htmlTableId = "ScopeInsights_CAFResourceNamingALL_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   CAF - Recommended abbreviations for Azure resource types docs
-   Resource details can be found in the CSV output *_ResourcesAll.csv
-   Download CSV semicolon | comma - - - - - - - - - - - - -"@) - $htmlScopeInsightsCAFResourceNamingALL = $null - $htmlScopeInsightsCAFResourceNamingALL = foreach ($entry in $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByType) { - - $resourceTypeGroupedByCAFResourceNamingResult = $entry.Group | Group-Object -Property cafResourceNamingResult, cafResourceNaming - if ($entry.Group.cafResourceNaming.Count -gt 1) { - $namingConvention = ($entry.Group.cafResourceNaming)[0] - $namingConventionFriendlyName = ($entry.Group.cafResourceNamingFriendlyName)[0] - } - else { - $namingConvention = $entry.Group.cafResourceNaming - $namingConventionFriendlyName = $entry.Group.cafResourceNamingFriendlyName - } - - $passed = 0 - $failed = 0 - foreach ($result in $resourceTypeGroupedByCAFResourceNamingResult) { - $resultNameSplitted = $result.Name -split ', ' - if ($resultNameSplitted[0] -eq 'passed') { - $passed = $result.Count - } - - if ($resultNameSplitted[0] -eq 'failed') { - $failed = $result.Count - } - } - - if ($passed -gt 0) { - $percentage = [math]::Round(($passed / ($passed + $failed) * 100), 2) - } - else { - $percentage = 0 - } - - @" - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsCAFResourceNamingALL) - [void]$htmlScopeInsights.AppendLine(@" - -
ResourceTypeRecommendationResourceFriendlyNamepassedfailedpassed percentage
$($entry.Name)$($namingConvention)$($namingConventionFriendlyName)$($passed)$($failed)$($percentage)%
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No CAF Naming Recommendation Compliance data available -'@) - } - - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - #endregion ScopeInsightsCAFResourceNamingALL - } - - #region ScopeInsightsOrphanedResources - if ($mgOrSub -eq 'sub') { - if ($arrayOrphanedResourcesGroupedBySubscription) { - $orphanedResourcesThisSubscription = $arrayOrphanedResourcesGroupedBySubscription.where({ $_.Name -eq $subscriptionId }) - if ($orphanedResourcesThisSubscription) { - $orphanedResourcesThisSubscriptionCount = $orphanedResourcesThisSubscription.Group.count - - if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { - $orphanedIncludingCost = $true - $hintTableTH = " ($($AzureConsumptionPeriod) days)" - - $orphanedResourcesThisSubscriptionGroupedByType = $orphanedResourcesThisSubscription.Group | Group-Object -Property type, currency - $orphanedResourcesThisSubscriptionGroupedByTypeCount = ($orphanedResourcesThisSubscriptionGroupedByType | Measure-Object).Count - } - else { - $orphanedIncludingCost = $false - $hintTableTH = '' - - $orphanedResourcesThisSubscriptionGroupedByType = $orphanedResourcesThisSubscription.Group | Group-Object -Property type - $orphanedResourcesThisSubscriptionGroupedByTypeCount = ($orphanedResourcesThisSubscriptionGroupedByType | Measure-Object).Count - } - - $tfCount = $orphanedResourcesThisSubscriptionGroupedByTypeCount - $htmlTableId = "ScopeInsights_OrphanedResources_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   'Azure Orphan Resources' ARG queries and workbooks GitHub
-   Resource details can be found in the CSV output *_ResourcesCostOptimizationAndCleanup.csv
-   Download CSV semicolon | comma - - - - - - - - - - - -"@) - $htmlScopeInsightsOrphanedResources = $null - $htmlScopeInsightsOrphanedResources = foreach ($resourceType in $orphanedResourcesThisSubscriptionGroupedByType | Sort-Object -Property Name) { - - if ($orphanedIncludingCost) { - if (($resourceType.Group[0].Intent) -like 'cost savings*') { - $orphCost = ($resourceType.Group.Cost | Measure-Object -Sum).Sum - if ($orphCost -eq 0) { - $orphCost = '' - } - $orphCurrency = $resourceType.Group[0].Currency - } - else { - $orphCost = '' - $orphCurrency = '' - } - } - else { - if (($resourceType.Group.Intent | Get-Unique) -like 'cost savings*') { - $orphCost = "use parameter -DoAzureConsumption to show potential savings" - $orphCurrency = '' - } - else { - $orphCost = '' - $orphCurrency = '' - } - } - - @" - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsOrphanedResources) - [void]$htmlScopeInsights.AppendLine(@" - -
ResourceTypeResource countIntentCost$($hintTableTH)Currency
$(($resourceType.Name -split ',')[0])$($resourceType.Group.Count)$($resourceType.Group[0].Intent)$($orphCost)$($orphCurrency)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No cost optimization & cleanup -'@) - } - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No cost optimization & cleanup -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - #endregion ScopeInsightsOrphanedResources - - #ScopeInsightsDiagnosticsCapable - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - #resourcesDiagnosticsCapable - #region ScopeInsightsDiagnosticsCapable - if ($mgOrSub -eq 'mg') { - $resourceTypesUnique = ($resourcesAllChildSubscriptions | Select-Object type -Unique).type - $resourceTypesSummarizedArray = [System.Collections.ArrayList]@() - foreach ($resourceTypeUnique in $resourceTypesUnique) { - $resourcesTypeCountTotal = 0 - ($resourcesAllChildSubscriptions.where( { $_.type -eq $resourceTypeUnique } )).count_ | ForEach-Object { $resourcesTypeCountTotal += $_ } - $dataFromResourceTypesDiagnosticsArray = $resourceTypesDiagnosticsArray.where( { $_.ResourceType -eq $resourceTypeUnique } ) - if ($dataFromResourceTypesDiagnosticsArray.Metrics -eq $true -or $dataFromResourceTypesDiagnosticsArray.Logs -eq $true) { - $resourceDiagnosticscapable = $true - } - else { - $resourceDiagnosticscapable = $false - } - $null = $resourceTypesSummarizedArray.Add([PSCustomObject]@{ - ResourceType = $resourceTypeUnique - ResourceCount = $resourcesTypeCountTotal - DiagnosticsCapable = $resourceDiagnosticscapable - Metrics = $dataFromResourceTypesDiagnosticsArray.Metrics - Logs = $dataFromResourceTypesDiagnosticsArray.Logs - LogCategories = ($dataFromResourceTypesDiagnosticsArray.LogCategories -join "$CsvDelimiterOpposite ") - }) - } - $subscriptionResourceTypesDiagnosticsCapableMetricsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true } )).count - $subscriptionResourceTypesDiagnosticsCapableLogsCount = ($resourceTypesSummarizedArray.where( { $_.Logs -eq $true } )).count - $subscriptionResourceTypesDiagnosticsCapableMetricsLogsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true -or $_.Logs -eq $true } )).count - - if ($resourcesAllChildSubscriptionResourceTypeCount -gt 0) { - $tfCount = $resourcesAllChildSubscriptionResourceTypeCount - $htmlTableId = "ScopeInsights_resourcesDiagnosticsCapable_$($mgchild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - - - -"@) - $htmlScopeInsightsDiagnosticsCapable = $null - $htmlScopeInsightsDiagnosticsCapable = foreach ($resourceSubscriptionResourceType in $resourceTypesSummarizedArray | Sort-Object @{Expression = { $_.ResourceType } }) { - @" - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsCapable) - [void]$htmlScopeInsights.AppendLine(@" - -
ResourceTypeResource CountDiagnostics capableMetricsLogsLogCategories
$($resourceSubscriptionResourceType.ResourceType)$($resourceSubscriptionResourceType.ResourceCount)$($resourceSubscriptionResourceType.DiagnosticsCapable)$($resourceSubscriptionResourceType.Metrics)$($resourceSubscriptionResourceType.Logs)$($resourceSubscriptionResourceType.LogCategories)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $resourcesAllChildSubscriptionResourceTypeCount ResourceTypes (1st party) Diagnostics capable (all Subscriptions below this scope) -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - - if ($mgOrSub -eq 'sub') { - $resourceTypesUnique = ($resourcesSubscription | Select-Object type -Unique).type - $resourceTypesSummarizedArray = [System.Collections.ArrayList]@() - foreach ($resourceTypeUnique in $resourceTypesUnique) { - $resourcesTypeCountTotal = 0 - ($resourcesSubscription.where( { $_.type -eq $resourceTypeUnique } )).count_ | ForEach-Object { $resourcesTypeCountTotal += $_ } - $dataFromResourceTypesDiagnosticsArray = $resourceTypesDiagnosticsArray.where( { $_.ResourceType -eq $resourceTypeUnique } ) - if ($dataFromResourceTypesDiagnosticsArray.Metrics -eq $true -or $dataFromResourceTypesDiagnosticsArray.Logs -eq $true) { - $resourceDiagnosticscapable = $true - } - else { - $resourceDiagnosticscapable = $false - } - $null = $resourceTypesSummarizedArray.Add([PSCustomObject]@{ - ResourceType = $resourceTypeUnique - ResourceCount = $resourcesTypeCountTotal - DiagnosticsCapable = $resourceDiagnosticscapable - Metrics = $dataFromResourceTypesDiagnosticsArray.Metrics - Logs = $dataFromResourceTypesDiagnosticsArray.Logs - LogCategories = ($dataFromResourceTypesDiagnosticsArray.LogCategories -join "$CsvDelimiterOpposite ") - }) - } - - $subscriptionResourceTypesDiagnosticsCapableMetricsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true } )).count - $subscriptionResourceTypesDiagnosticsCapableLogsCount = ($resourceTypesSummarizedArray.where( { $_.Logs -eq $true } )).count - $subscriptionResourceTypesDiagnosticsCapableMetricsLogsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true -or $_.Logs -eq $true } )).count - - if ($resourcesSubscriptionResourceTypeCount -gt 0) { - $tfCount = $resourcesSubscriptionResourceTypeCount - $htmlTableId = "ScopeInsights_resourcesDiagnosticsCapable_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - - - -"@) - $htmlScopeInsightsDiagnosticsCapable = $null - $htmlScopeInsightsDiagnosticsCapable = foreach ($resourceSubscriptionResourceType in $resourceTypesSummarizedArray | Sort-Object @{Expression = { $_.ResourceType } }) { - @" - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsCapable) - [void]$htmlScopeInsights.AppendLine(@" - -
ResourceTypeResource CountDiagnostics capableMetricsLogsLogCategories
$($resourceSubscriptionResourceType.ResourceType)$($resourceSubscriptionResourceType.ResourceCount)$($resourceSubscriptionResourceType.DiagnosticsCapable)$($resourceSubscriptionResourceType.Metrics)$($resourceSubscriptionResourceType.Logs)$($resourceSubscriptionResourceType.LogCategories)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $resourcesSubscriptionResourceTypeCount ResourceTypes (1st party) Diagnostics capable -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - #endregion ScopeInsightsDiagnosticsCapable - } - - #ScopeInsightsUserAssignedIdentities4Resources - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - if ($mgOrSub -eq 'sub') { - #region ScopeInsightsUserAssignedIdentities4Resources - if ($arrayUserAssignedIdentities4ResourcesSubscriptionCount -gt 0) { - $tfCount = $arrayUserAssignedIdentities4ResourcesSubscriptionCount - $htmlTableId = "ScopeInsights_UserAssignedIdentities4Resources_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Managed identity 'user-assigned' vs 'system-assigned' docs
-   Download CSV semicolon | comma - - - - - - - - - - - - - - - - - - - - - - - - -"@) - $htmlScopeInsightsUserAssignedIdentities4Resource = $null - $htmlScopeInsightsUserAssignedIdentities4Resource = foreach ($miResEntry in $arrayUserAssignedIdentities4ResourcesSubscription | Sort-Object -Property miResourceId, resourceId) { - @" - - - - - - - - - - - - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsUserAssignedIdentities4Resource) - [void]$htmlScopeInsights.AppendLine(@" - -
MI NameMI MgPathMI Subscription NameMI Subscription IdMI ResourceGroupMI ResourceIdMI AAD SP objectIdMI AAD SP applicationIdMI count Res assignmentsMI used cross subscriptionRes NameRes TypeRes MgPathRes Subscription NameRes Subscription IdRes ResourceGroupRes IdRes count assigned MIs
$($miResEntry.miResourceName)$($miResEntry.miMgPath)$($miResEntry.miSubscriptionName)$($miResEntry.miSubscriptionId)$($miResEntry.miResourceGroupName)$($miResEntry.miResourceId)$($miResEntry.miPrincipalId)$($miResEntry.miClientId)$($htUserAssignedIdentitiesAssignedResources.($miResEntry.miPrincipalId).ResourcesCount)$($miResEntry.miCrossSubscription)$($miResEntry.resourceName)$($miResEntry.resourceType)$($miResEntry.resourceMgPath)$($miResEntry.resourceSubscriptionName)$($miResEntry.resourceSubscriptionId)$($miResEntry.resourceResourceGroupName)$($miResEntry.resourceId)$($htResourcesAssignedUserAssignedIdentities.(($miResEntry.resourceId).tolower()).UserAssignedIdentitiesCount)
- -
-"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No UserAssigned Managed Identities assigned to Resources / vice versa - at all -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsUserAssignedIdentities4Resources - } - } - - #ScopeInsightsPSRule - if ($azAPICallConf['htParameters'].NoResources -eq $false) { - if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { - #region ScopeInsightsPSRule - - if ($mgOrSub -eq 'mg') { - - $allPSRuleResultsUnderThisMg = [system.collections.ArrayList]@() - foreach ($mg in $grpPSRuleManagementGroups) { - if ($htManagementGroupsMgPath.($mg.name -replace '.*/').path -contains $mgchild) { - $allPSRuleResultsUnderThisMg.AddRange($mg.Group) - } - } - - $grpThisManagementGroup = $allPSRuleResultsUnderThisMg | Group-Object -Property resourceType, pillar, category, severity, rule, result - - if ($grpThisManagementGroup) { - $grpThisManagementGroupCount = $grpThisManagementGroup.Count - $tfCount = $grpThisManagementGroupCount - $htmlTableId = "ScopeInsights_PSRule_$($mgchild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Learn about PSRule for Azure
-   Download CSV semicolon | comma - - - - - - - - - - - - - - - - -"@) - $htmlScopeInsightsPSRuleMG = $null - $htmlScopeInsightsPSRuleMG = foreach ($result in $grpThisManagementGroup) { - $resultNameSplit = $result.Name.split(', ') - @" - - - - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPSRuleMG) - [void]$htmlScopeInsights.AppendLine(@" - -
Resource TypeResource CountSubscription CountPillarCategorySeverityRuleRecommendationlnkState
$($resultNameSplit[0])$($result.Group.Count)$(($result.Group.subscriptionId | Sort-Object -Unique).Count)$($resultNameSplit[1])$($resultNameSplit[2])$($resultNameSplit[3])$(($result.Group[0].rule))$(($result.Group[0].recommendation))$($resultNameSplit[5])
- -
-"@) - - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No PSRule for Azure results -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - - if ($mgOrSub -eq 'sub') { - $grpThisSubscription = $grpPSRuleSubscriptions.where({ $_.Name -eq $subscriptionId }) - $grpThisSubscriptionGrouped = $grpThisSubscription.Group | Group-Object -Property resourceType, pillar, category, severity, rule, result - - if ($grpThisSubscriptionGrouped) { - $grpThisSubscriptionGroupedCount = $grpThisSubscriptionGrouped.Count - $tfCount = $grpThisSubscriptionGroupedCount - $htmlTableId = "ScopeInsights_PSRule_$($subscriptionId -replace '-','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Learn about PSRule for Azure
-   Download CSV semicolon | comma - - - - - - - - - - - - - - - -"@) - $htmlScopeInsightsPSRuleSub = $null - $htmlScopeInsightsPSRuleSub = foreach ($result in $grpThisSubscriptionGrouped) { - $resultNameSplit = $result.Name.split(', ') - @" - - - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPSRuleSub) - [void]$htmlScopeInsights.AppendLine(@" - -
Resource TypeResource CountPillarCategorySeverityRuleRecommendationlnkState
$($resultNameSplit[0])$($result.Group.Count)$($resultNameSplit[1])$($resultNameSplit[2])$($resultNameSplit[3])$(($result.Group[0].rule))$(($result.Group[0].recommendation))$($resultNameSplit[5])
- -
-"@) - - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No PSRule results -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - #endregion ScopeInsightsPSRule - } - else { - [void]$htmlScopeInsights.AppendLine(@' - PSRule for Azure - integration paused - PSRule for Azure -'@) - } - } - - #PolicyAssignments - #region ScopeInsightsPolicyAssignments - if ($mgOrSub -eq 'mg') { - $SIDivContentClass = 'contentSIMG' - $htmlTableIdentifier = $mgChild - - $policiesAssigned = [System.Collections.ArrayList]@() - $policiesCount = 0 - $policiesCountBuiltin = 0 - $policiesCountCustom = 0 - $policiesAssignedAtScope = 0 - $policiesInherited = 0 - foreach ($policyAssignment in $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicy) { - if ([String]::IsNullOrEmpty($policyAssignment.subscriptionId)) { - $null = $policiesAssigned.Add($policyAssignment) - $policiesCount++ - if ($policyAssignment.PolicyType -eq 'BuiltIn') { - $policiesCountBuiltin++ - } - if ($policyAssignment.PolicyType -eq 'Custom') { - $policiesCountCustom++ - } - if ($policyAssignment.Inheritance -like 'this*') { - $policiesAssignedAtScope++ - } - if ($policyAssignment.Inheritance -notlike 'this*') { - $policiesInherited++ - } - } - } - } - if ($mgOrSub -eq 'sub') { - $SIDivContentClass = 'contentSISub' - $htmlTableIdentifier = $subscriptionId - - $policiesAssigned = [System.Collections.ArrayList]@() - $policiesCount = 0 - $policiesCountBuiltin = 0 - $policiesCountCustom = 0 - $policiesAssignedAtScope = 0 - $policiesInherited = 0 - foreach ($policyAssignment in $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicy) { - $null = $policiesAssigned.Add($policyAssignment) - $policiesCount++ - if ($policyAssignment.PolicyType -eq 'BuiltIn') { - $policiesCountBuiltin++ - } - if ($policyAssignment.PolicyType -eq 'Custom') { - $policiesCountCustom++ - } - if ($policyAssignment.Inheritance -like 'this*') { - $policiesAssignedAtScope++ - } - if ($policyAssignment.Inheritance -notlike 'this*') { - $policiesInherited++ - } - } - } - - if (($policiesAssigned).count -gt 0) { - $tfCount = ($policiesAssigned).count - $htmlTableId = "ScopeInsights_PolicyAssignments_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - $noteOrNot = '' - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma
-  *Depending on the number of rows and your computer´s performance the table may respond with delay, download the csv for better filtering experience - - - - - - - - - - - - - - - -"@) - - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - - [void]$htmlScopeInsights.AppendLine(@' - - - - - -'@) - } - - [void]$htmlScopeInsights.AppendLine(@" - - - - - - - - - - - - -"@) - $htmlScopeInsightsPolicyAssignments = $null - $htmlScopeInsightsPolicyAssignments = foreach ($policyAssignment in $policiesAssigned | Sort-Object @{Expression = { $_.Level } }, @{Expression = { $_.MgName } }, @{Expression = { $_.MgId } }, @{Expression = { $_.SubscriptionName } }, @{Expression = { $_.SubscriptionId } }, @{Expression = { $_.PolicyAssignmentId } }) { - - if ($policyAssignment.PolicyType -eq 'Custom') { - $policyName = ($policyAssignment.PolicyName -replace '<', '<' -replace '>', '>') - } - else { - $policyName = $policyAssignment.PolicyName - } - @" - - - - - - - - - - - - - -"@ - - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - @" - - - - - -"@ - } - - @" - - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPolicyAssignments) - [void]$htmlScopeInsights.AppendLine(@" - -
InheritanceScopeExcludedExemption appliesPolicy DisplayNamePolicyIdTypeCategoryALZEffectParametersEnforcementNonCompliance MessagePolicies NonCmplntPolicies CompliantResources NonCmplntResources CompliantResources ConflictingRole/Assignment $noteOrNotManaged IdentityAssignment DisplayNameAssignmentIdAssignedByCreatedOnCreatedByUpdatedOnUpdatedBy
$($policyAssignment.Inheritance)$($policyAssignment.ExcludedScope)$($policyAssignment.ExemptionScope)$($policyName)$($policyAssignment.PolicyId)$($policyAssignment.PolicyType)$($policyAssignment.PolicyCategory -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyIsALZ)$($policyAssignment.Effect)$($policyAssignment.PolicyAssignmentParameters)$($policyAssignment.PolicyAssignmentEnforcementMode)$($policyAssignment.PolicyAssignmentNonComplianceMessages)$($policyAssignment.NonCompliantPolicies)$($policyAssignment.CompliantPolicies)$($policyAssignment.NonCompliantResources)$($policyAssignment.CompliantResources)$($policyAssignment.ConflictingResources)$($policyAssignment.RelatedRoleAssignments)$($policyAssignment.PolicyAssignmentMI)$($policyAssignment.PolicyAssignmentDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentId -replace '<', '<' -replace '>', '>')$($policyAssignment.AssignedBy)$($policyAssignment.CreatedOn)$($policyAssignment.CreatedBy)$($policyAssignment.UpdatedOn)$($policyAssignment.UpdatedBy)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $(($policiesAssigned).count) Policy assignments -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsPolicyAssignments - - #PolicySetAssignments - #region ScopeInsightsPolicySetAssignments - if ($mgOrSub -eq 'mg') { - $SIDivContentClass = 'contentSIMG' - $htmlTableIdentifier = $mgChild - - $policySetsAssigned = [System.Collections.ArrayList]@() - $policySetsCount = 0 - $policySetsCountBuiltin = 0 - $policySetsCountCustom = 0 - $policySetsAssignedAtScope = 0 - $policySetsInherited = 0 - foreach ($policySetAssignment in $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicySet) { - if ([String]::IsNullOrEmpty($policySetAssignment.subscriptionId)) { - $null = $policySetsAssigned.Add($policySetAssignment) - $policySetsCount++ - if ($policySetAssignment.PolicyType -eq 'BuiltIn') { - $policySetsCountBuiltin++ - } - if ($policySetAssignment.PolicyType -eq 'Custom') { - $policySetsCountCustom++ - } - if ($policySetAssignment.Inheritance -like 'this*') { - $policySetsAssignedAtScope++ - } - if ($policySetAssignment.Inheritance -notlike 'this*') { - $policySetsInherited++ - } - } - } - } - if ($mgOrSub -eq 'sub') { - $SIDivContentClass = 'contentSISub' - $htmlTableIdentifier = $subscriptionId - - $policySetsAssigned = [System.Collections.ArrayList]@() - $policySetsCount = 0 - $policySetsCountBuiltin = 0 - $policySetsCountCustom = 0 - $policySetsAssignedAtScope = 0 - $policySetsInherited = 0 - foreach ($policySetAssignment in $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicySet) { - $null = $policySetsAssigned.Add($policySetAssignment) - $policySetsCount++ - if ($policySetAssignment.PolicyType -eq 'BuiltIn') { - $policySetsCountBuiltin++ - } - if ($policySetAssignment.PolicyType -eq 'Custom') { - $policySetsCountCustom++ - } - if ($policySetAssignment.Inheritance -like 'this*') { - $policySetsAssignedAtScope++ - } - if ($policySetAssignment.Inheritance -notlike 'this*') { - $policySetsInherited++ - } - } - } - - if (($policySetsAssigned).count -gt 0) { - $tfCount = ($policiesAssigned).count - $htmlTableId = "ScopeInsights_PolicySetAssignments_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - $noteOrNot = '' - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - - - - -"@) - - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - - [void]$htmlScopeInsights.AppendLine(@' - - - - - -'@) - } - - [void]$htmlScopeInsights.AppendLine(@" - - - - - - - - - - - - -"@) - $htmlScopeInsightsPolicySetAssignments = $null - $htmlScopeInsightsPolicySetAssignments = foreach ($policyAssignment in $policySetsAssigned | Sort-Object -Property Level, PolicyAssignmentId) { - if ($policyAssignment.PolicyType -eq 'Custom') { - $policyName = ($policyAssignment.PolicyName -replace '<', '<' -replace '>', '>') - } - else { - $policyName = $policyAssignment.PolicyName - } - @" - - - - - - - - - - - -"@ - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - @" - - - - - -"@ - } - @" - - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPolicySetAssignments) - [void]$htmlScopeInsights.AppendLine(@" - -
InheritanceScopeExcludedPolicySet DisplayNamePolicySetIdTypeCategoryALZParametersEnforcementNonCompliance MessagePolicies NonCmplntPolicies CompliantResources NonCmplntResources CompliantResources ConflictingRole/Assignment $noteOrNotManaged IdentityAssignment DisplayNameAssignmentIdAssignedByCreatedOnCreatedByUpdatedOnUpdatedBy
$($policyAssignment.Inheritance)$($policyAssignment.ExcludedScope)$($policyName)$($policyAssignment.PolicyId)$($policyAssignment.PolicyType)$($policyAssignment.PolicyCategory -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyIsALZ)$($policyAssignment.PolicyAssignmentParameters)$($policyAssignment.PolicyAssignmentEnforcementMode)$($policyAssignment.PolicyAssignmentNonComplianceMessages)$($policyAssignment.NonCompliantPolicies)$($policyAssignment.CompliantPolicies)$($policyAssignment.NonCompliantResources)$($policyAssignment.CompliantResources)$($policyAssignment.ConflictingResources)$($policyAssignment.RelatedRoleAssignments)$($policyAssignment.PolicyAssignmentMI)$($policyAssignment.PolicyAssignmentDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentId -replace '<', '<' -replace '>', '>')$($policyAssignment.AssignedBy)$($policyAssignment.CreatedOn)$($policyAssignment.CreatedBy)$($policyAssignment.UpdatedOn)$($policyAssignment.UpdatedBy)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $(($policySetsAssigned).count) PolicySet assignments -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsPolicySetAssignments - - #PolicyAssignmentsLimit (Policy+PolicySet) - #region ScopeInsightsPolicyAssignmentsLimit - if ($mgOrSub -eq 'mg') { - $limit = $LimitPOLICYPolicyAssignmentsManagementGroup - } - if ($mgOrSub -eq 'sub') { - $limit = $LimitPOLICYPolicyAssignmentsSubscription - } - - if ($policiesAssignedAtScope -eq 0 -and $policySetsAssignedAtScope -eq 0) { - $faimage = "" - - [void]$htmlScopeInsights.AppendLine(@" - $faImage Policy Assignment Limit: 0/$limit -"@) - } - else { - if ($mgOrSub -eq 'mg') { - $scopePolicyAssignmentsLimit = $policyPolicyBaseQueryScopeInsights.where( { [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.MgId -eq $mgChild } ) - } - if ($mgOrSub -eq 'sub') { - $scopePolicyAssignmentsLimit = $policyPolicyBaseQueryScopeInsights.where( { $_.SubscriptionId -eq $subscriptionId } ) - } - - if ($scopePolicyAssignmentsLimit.PolicyAndPolicySetAssignmentAtScopeCount -gt (($limit) * $LimitCriticalPercentage / 100)) { - $faImage = "" - } - else { - $faimage = "" - } - [void]$htmlScopeInsights.AppendLine(@" - $faImage Policy Assignment Limit: $($scopePolicyAssignmentsLimit.PolicyAndPolicySetAssignmentAtScopeCount)/$($limit) -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsPolicyAssignmentsLimit - - #ScopedPolicies - #region ScopeInsightsScopedPolicies - if ($mgOrSub -eq 'mg') { - $SIDivContentClass = 'contentSIMG' - $htmlTableIdentifier = $mgChild - $scopePolicies = $customPoliciesDetailed.where( { $_.PolicyDefinitionId -like "*/providers/Microsoft.Management/managementGroups/$mgChild/*" } ) - $scopePoliciesCount = ($scopePolicies).count - } - if ($mgOrSub -eq 'sub') { - $SIDivContentClass = 'contentSISub' - $htmlTableIdentifier = $subscriptionId - $scopePolicies = $customPoliciesDetailed.where( { $_.PolicyDefinitionId -like "*/subscriptions/$subscriptionId/*" } ) - $scopePoliciesCount = ($scopePolicies).count - } - - if ($scopePoliciesCount -gt 0) { - $tfCount = $scopePoliciesCount - $htmlTableId = "ScopeInsights_ScopedPolicies_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - if ($mgOrSub -eq 'mg') { - $LimitPOLICYPolicyScoped = $LimitPOLICYPolicyDefinitionsScopedManagementGroup - if ($scopePoliciesCount -gt (($LimitPOLICYPolicyScoped * $LimitCriticalPercentage) / 100)) { - $faIcon = "" - } - else { - $faIcon = "" - } - } - if ($mgOrSub -eq 'sub') { - $LimitPOLICYPolicyScoped = $LimitPOLICYPolicyDefinitionsScopedSubscription - if ($scopePoliciesCount -gt (($LimitPOLICYPolicyScoped * $LimitCriticalPercentage) / 100)) { - $faIcon = "" - } - else { - $faIcon = "" - } - } - - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - - - - - -"@) - $htmlScopeInsightsScopedPolicies = $null - $htmlScopeInsightsScopedPolicies = foreach ($custompolicy in $scopePolicies | Sort-Object @{Expression = { $_.PolicyDisplayName } }, @{Expression = { $_.PolicyDefinitionId } }) { - if ($custompolicy.UsedInPolicySetsCount -gt 0) { - $customPolicyUsedInPolicySets = "$($customPolicy.UsedInPolicySetsCount) ($($customPolicy.UsedInPolicySets))" - } - else { - $customPolicyUsedInPolicySets = $($customPolicy.UsedInPolicySetsCount) - } - @" - - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsScopedPolicies) - [void]$htmlScopeInsights.AppendLine(@" - -
Policy DisplayNamePolicyIdCategoryALZPolicy effectRole definitionsUnique assignmentsUsed in PolicySets
$($customPolicy.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionId -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyCategory -replace '<', '<' -replace '>', '>')$($customPolicy.ALZ)$($customPolicy.PolicyEffect)$($customPolicy.RoleDefinitions)$($customPolicy.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicyUsedInPolicySets)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $scopePoliciesCount Custom Policy definitions scoped -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsScopedPolicies - - #ScopedPolicySets - #region ScopeInsightsScopedPolicySets - if ($mgOrSub -eq 'mg') { - $SIDivContentClass = 'contentSIMG' - $htmlTableIdentifier = $mgChild - $scopePolicySets = $customPolicySetsDetailed.where( { $_.PolicySetDefinitionId -like "*/providers/Microsoft.Management/managementGroups/$mgChild/*" } ) - $scopePolicySetsCount = ($scopePolicySets).count - } - if ($mgOrSub -eq 'sub') { - $SIDivContentClass = 'contentSISub' - $htmlTableIdentifier = $subscriptionId - $scopePolicySets = $customPolicySetsDetailed.where( { $_.PolicySetDefinitionId -like "*/subscriptions/$subscriptionId/*" } ) - $scopePolicySetsCount = ($scopePolicySets).count - } - - if ($scopePolicySetsCount -gt 0) { - $tfCount = $scopePolicySetsCount - $htmlTableId = "ScopeInsights_ScopedPolicySets_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - if ($mgOrSub -eq 'mg') { - $LimitPOLICYPolicySetScoped = $LimitPOLICYPolicySetDefinitionsScopedManagementGroup - if ($scopePolicySetsCount -gt (($LimitPOLICYPolicySetScoped * $LimitCriticalPercentage) / 100)) { - $faIcon = "" - } - else { - $faIcon = "" - } - } - if ($mgOrSub -eq 'sub') { - $LimitPOLICYPolicySetScoped = $LimitPOLICYPolicySetDefinitionsScopedSubscription - if ($scopePolicySetsCount -gt (($LimitPOLICYPolicySetScoped * $LimitCriticalPercentage) / 100)) { - $faIcon = "" - } - else { - $faIcon = "" - } - } - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - - - -"@) - $htmlScopeInsightsScopedPolicySets = $null - $htmlScopeInsightsScopedPolicySets = foreach ($custompolicySet in $scopePolicySets | Sort-Object @{Expression = { $_.PolicySetDisplayName } }, @{Expression = { $_.PolicySetDefinitionId } }) { - @" - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsScopedPolicySets) - [void]$htmlScopeInsights.AppendLine(@" - -
PolicySet DisplayNamePolicySetIdCategoryALZUnique assignmentsPolicies Used
$($custompolicySet.PolicySetDisplayName -replace '<', '<' -replace '>', '>')$($custompolicySet.PolicySetDefinitionId -replace '<', '<' -replace '>', '>')$($custompolicySet.PolicySetCategory -replace '<', '<' -replace '>', '>')$($custompolicySet.ALZ)$($custompolicySet.UniqueAssignments -replace '<', '<' -replace '>', '>')$($custompolicySet.PoliciesUsed)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $scopePolicySetsCount Custom PolicySet definitions scoped -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsScopedPolicySets - - #BlueprintAssignments - #region ScopeInsightsBlueprintAssignments - if ($mgOrSub -eq 'sub') { - if ($blueprintsAssignedCount -gt 0) { - - if ($mgOrSub -eq 'mg') { - $htmlTableIdentifier = $mgChild - } - if ($mgOrSub -eq 'sub') { - $htmlTableIdentifier = $subscriptionId - } - $htmlTableId = "ScopeInsights_BlueprintAssignment_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - - - -"@) - $htmlScopeInsightsBlueprintAssignments = $null - $htmlScopeInsightsBlueprintAssignments = foreach ($blueprintAssigned in $blueprintsAssigned) { - @" - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsBlueprintAssignments) - [void]$htmlScopeInsights.AppendLine(@" - -
Blueprint NameBlueprint DisplayNameBlueprint DescriptionBlueprintIdBlueprint VersionBlueprint AssignmentId
$($blueprintAssigned.BlueprintName -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintDisplayName -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintDescription -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintId -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintAssignmentVersion -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintAssignmentId -replace '<', '<' -replace '>', '>')
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $blueprintsAssignedCount Blueprints assigned -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - } - #endregion ScopeInsightsBlueprintAssignments - - #BlueprintsScoped - #region ScopeInsightsBlueprintsScoped - if ($blueprintsScopedCount -gt 0) { - $tfCount = $blueprintsScopedCount - if ($mgOrSub -eq 'mg') { - $SIDivContentClass = 'contentSIMG' - $htmlTableIdentifier = $mgChild - } - if ($mgOrSub -eq 'sub') { - $SIDivContentClass = 'contentSISub' - $htmlTableIdentifier = $subscriptionId - } - $htmlTableId = "ScopeInsights_BlueprintScoped_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - - - -"@) - $htmlScopeInsightsBlueprintsScoped = $null - $htmlScopeInsightsBlueprintsScoped = foreach ($blueprintScoped in $blueprintsScoped) { - @" - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsBlueprintsScoped) - [void]$htmlScopeInsights.AppendLine(@" - -
Blueprint NameBlueprint DisplayNameBlueprint DescriptionBlueprintId
$($blueprintScoped.BlueprintName -replace '<', '<' -replace '>', '>')$($blueprintScoped.BlueprintDisplayName -replace '<', '<' -replace '>', '>')$($blueprintScoped.BlueprintDescription -replace '<', '<' -replace '>', '>')$($blueprintScoped.BlueprintId -replace '<', '<' -replace '>', '>')
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $blueprintsScopedCount Blueprints scoped -"@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsBlueprintsScoped - - if ($mgOrSub -eq 'sub') { - #region ScopeInsightsClassicAdministrators - if ($htClassicAdministrators.($subscriptionId).ClassicAdministrators.Count -gt 0) { - $tfCount = $htClassicAdministrators.($subscriptionId).ClassicAdministrators.Count - $htmlTableId = "ScopeInsights_ClassicAdministrators_$($subscriptionId -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma - - - - - - - - -"@) - $htmlScopeInsightsClassicAdministrators = $null - $htmlScopeInsightsClassicAdministrators = foreach ($classicAdministrator in $htClassicAdministrators.($subscriptionId).ClassicAdministrators | Sort-Object -Property Role, Identity) { - @" - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsClassicAdministrators) - [void]$htmlScopeInsights.AppendLine(@" - -
RoleIdentity
$($classicAdministrator.Role)$($classicAdministrator.Identity)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@' - No Classic Administrators -'@) - } - [void]$htmlScopeInsights.AppendLine(@' -
-'@) - #endregion ScopeInsightsClassicAdministrators - } - - #RoleAssignments - #region ScopeInsightsRoleAssignments - if ($mgOrSub -eq 'mg') { - $SIDivContentClass = 'contentSIMG' - $htmlTableIdentifier = $mgChild - $LimitRoleAssignmentsScope = $LimitRBACRoleAssignmentsManagementGroup - - $rolesAssigned = [System.Collections.ArrayList]@() - $rolesAssignedCount = 0 - $rolesAssignedInheritedCount = 0 - $rolesAssignedUser = 0 - $rolesAssignedGroup = 0 - $rolesAssignedServicePrincipal = 0 - $rolesAssignedUnknown = 0 - $roleAssignmentsRelatedToPolicyCount = 0 - $roleSecurityFindingCustomRoleOwner = 0 - $roleSecurityFindingOwnerAssignmentSP = 0 - $rbacForThisManagementGroup = ($rbacAllGroupedByManagementGroup.where( { $_.name -eq $mgChild } )).group - foreach ($roleAssignment in $rbacForThisManagementGroup) { - if ([String]::IsNullOrEmpty($roleAssignment.subscriptionId)) { - $null = $rolesAssigned.Add($roleAssignment) - $rolesAssignedCount++ - if ($roleAssignment.Scope -notlike 'this*') { - $rolesAssignedInheritedCount++ - } - if ($roleAssignment.ObjectType -like 'User*') { - $rolesAssignedUser++ - } - if ($roleAssignment.ObjectType -eq 'Group') { - $rolesAssignedGroup++ - } - if ($roleAssignment.ObjectType -like 'SP*') { - $rolesAssignedServicePrincipal++ - } - if ($roleAssignment.ObjectType -eq 'Unknown') { - $rolesAssignedUnknown++ - } - if ($roleAssignment.RbacRelatedPolicyAssignment -ne 'none') { - $roleAssignmentsRelatedToPolicyCount++ - } - if ($roleAssignment.RoleSecurityCustomRoleOwner -eq 1) { - $roleSecurityFindingCustomRoleOwner++ - } - if ($roleAssignment.RoleSecurityOwnerAssignmentSP -eq 1) { - $roleSecurityFindingOwnerAssignmentSP++ - } - } - } - } - if ($mgOrSub -eq 'sub') { - $SIDivContentClass = 'contentSISub' - $htmlTableIdentifier = $subscriptionId - $LimitRoleAssignmentsScope = $htSubscriptionsRoleAssignmentLimit.($subscriptionId) - - $rolesAssigned = [System.Collections.ArrayList]@() - $rolesAssignedCount = 0 - $rolesAssignedInheritedCount = 0 - $rolesAssignedUser = 0 - $rolesAssignedGroup = 0 - $rolesAssignedServicePrincipal = 0 - $rolesAssignedUnknown = 0 - $roleAssignmentsRelatedToPolicyCount = 0 - $roleSecurityFindingCustomRoleOwner = 0 - $roleSecurityFindingOwnerAssignmentSP = 0 - $rbacForThisSubscription = ($rbacAllGroupedBySubscription.where( { $_.name -eq $subscriptionId } )).group - $rolesAssigned = foreach ($roleAssignment in $rbacForThisSubscription) { - - $roleAssignment - $rolesAssignedCount++ - if ($roleAssignment.Scope -notlike 'this*') { - $rolesAssignedInheritedCount++ - } - if ($roleAssignment.ObjectType -like 'User*') { - $rolesAssignedUser++ - } - if ($roleAssignment.ObjectType -eq 'Group') { - $rolesAssignedGroup++ - } - if ($roleAssignment.ObjectType -like 'SP*') { - $rolesAssignedServicePrincipal++ - } - if ($roleAssignment.ObjectType -eq 'Unknown') { - $rolesAssignedUnknown++ - } - if ($roleAssignment.RbacRelatedPolicyAssignment -ne 'none') { - $roleAssignmentsRelatedToPolicyCount++ - } - if ($roleAssignment.RoleSecurityCustomRoleOwner -eq 1) { - $roleSecurityFindingCustomRoleOwner++ - } - if ($roleAssignment.RoleSecurityOwnerAssignmentSP -eq 1) { - $roleSecurityFindingOwnerAssignmentSP++ - } - } - } - - $rolesAssignedAtScopeCount = $rolesAssignedCount - $rolesAssignedInheritedCount - - if (($rolesAssigned).count -gt 0) { - $tfCount = ($rolesAssigned).count - $htmlTableId = "ScopeInsights_RoleAssignments_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" - $randomFunctionName = "func_$htmlTableId" - $noteOrNot = '' - [void]$htmlScopeInsights.AppendLine(@" - -
-   Download CSV semicolon | comma
-  *Depending on the number of rows and your computer´s performance the table may respond with delay, download the csv for better filtering experience - - - - - - - - - - - - - - - - - - - - - - - -"@) - $htmlScopeInsightsRoleAssignments = $null - $htmlScopeInsightsRoleAssignments = foreach ($roleAssignment in ($rolesAssigned | Sort-Object -Property Level, MgName, MgId, SubscriptionName, SubscriptionId, Scope, Role, RoleId, ObjectId, RoleAssignmentId)) { - @" - - - - - - - - - - - - - - - - - - - -"@ - } - [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsRoleAssignments) - [void]$htmlScopeInsights.AppendLine(@" - -
ScopeRoleRoleIdRole TypeDataCan do Role assignmentIdentity DisplaynameIdentity SignInNameIdentity ObjectIdIdentity TypeApplicabilityApplies through membership Group DetailsRole AssignmentIdRelated Policy Assignment $noteOrNotCreatedOnCreatedBy
$($roleAssignment.Scope)$($roleAssignment.Role)$($roleAssignment.RoleId)$($roleAssignment.RoleType)$($roleAssignment.RoleDataRelated)$($roleAssignment.RoleCanDoRoleAssignments)$($roleAssignment.ObjectDisplayName)$($roleAssignment.ObjectSignInName)$($roleAssignment.ObjectId)$($roleAssignment.ObjectType)$($roleAssignment.AssignmentType)$($roleAssignment.AssignmentInheritFrom)$($roleAssignment.GroupMembersCount)$($roleAssignment.RoleAssignmentId)$($roleAssignment.rbacRelatedPolicyAssignment)$($roleAssignment.CreatedOn)$($roleAssignment.CreatedBy)
-
- -"@) - } - else { - [void]$htmlScopeInsights.AppendLine(@" - $(($rbacAll).count) Role assignments - -"@) - } - - [void]$htmlScopeInsights.AppendLine(@' -
- - - - - - - - - - - - - - - - - - - - -"@) - $htmlSUMMARYcustompolicies = $null - $htmlSUMMARYcustompolicies = foreach ($customPolicy in ($customPoliciesDetailed | Sort-Object @{Expression = { $_.PolicyDisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { - if ($custompolicy.UsedInPolicySetsCount -gt 0) { - $customPolicyUsedInPolicySets = "$($customPolicy.UsedInPolicySetsCount) ($($customPolicy.UsedInPolicySets))" - } - else { - $customPolicyUsedInPolicySets = $($customPolicy.UsedInPolicySetsCount) - } - @" - - - - - - - - - - - - - - - - - -"@ - } - - [void]$htmlTenantSummary.AppendLine($htmlSUMMARYcustompolicies) - $htmlTenantSummary | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force - $htmlTenantSummary = [System.Text.StringBuilder]::new() - [void]$htmlTenantSummary.AppendLine(@" - -
ScopeScope IdPolicy DisplayNamePolicy NamePolicyIdCategoryALZEffectRole definitionsUnique assignmentsUsed in PolicySetsCreatedOnCreatedByUpdatedOnUpdatedBy
$($customPolicy.Scope)$($customPolicy.ScopeId)$($customPolicy.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionId -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyCategory -replace '<', '<' -replace '>', '>')$($customPolicy.ALZ)$($customPolicy.PolicyEffect)$($customPolicy.RoleDefinitions)$($customPolicy.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicyUsedInPolicySets)$($customPolicy.CreatedOn)$($customPolicy.CreatedBy)$($customPolicy.UpdatedOn)$($customPolicy.UpdatedBy)
-
- -"@) - } - else { - [void]$htmlTenantSummary.AppendLine(@" -

$tenantCustomPoliciesCount Custom Policy definitions ($scopeNamingSummary)

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

$tenantCustomPoliciesCount Custom Policy definitions ($scopeNamingSummary)

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

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

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

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

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

$tenantCustomPolicySetsCount Custom PolicySet definitions ($scopeNamingSummary)

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

$tenantCustomPolicySetsCount Custom PolicySet definitions ($scopeNamingSummary)

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

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

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

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

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

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

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

Azure Landing Zones (ALZ) Policy Version Checker

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

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

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

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

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

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

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

$($policyExemptionsCount) Policy exemptions

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

$($policyAssignmentsOrphanedCount) Policy assignments orphaned

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

$(($arrayPolicyAssignmentsEnriched).count) Policy assignments

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

No Policies to remediate

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

$tenantCustomRolesCount Custom Role definitions ($scopeNamingSummary)

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

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

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

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

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

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

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

No ClassicAdministrators

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

$($rbacAllCount) Role assignments

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

No PIM Eligibility

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

No PIM Eligibility - parameter -NoPIMEligibility = $NoPIMEligibility

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

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

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

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

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

$($tenantAllRolesCanDoRoleAssignmentsCount) Role definitions can apply Role assignments

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

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

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

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

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

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

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

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

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

$blueprintDefinitionsCount Blueprint definitions

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

$blueprintAssignmentsCount Blueprint assignments

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

$blueprintDefinitionsOrphanedCount Orphaned Blueprint definitions

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

$($summaryManagementGroupsCount) Management Groups

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

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

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

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

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

$($summarySubscriptionsCount) Subscriptions

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

$outOfScopeSubscriptionsCount Subscriptions out-of-scope

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

Tag Name Usage ($tagsUsageCount Tags) docs

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

Resources ($resourcesResourceTypeCount ResourceTypes)

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

Resources (0 ResourceTypes)

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

Resources ($resourcesResourceTypeCount ResourceTypes)

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

Resources (0 ResourceTypes)

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

No Resource fluctuation since last run

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

No CAF Naming Recommendation Compliance data

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

No cost optimization & cleanup

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

$resourceProvidersAllCount Resource Providers

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

$resourceProvidersAllCount Resource Providers

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

No enabled Subscriptions Features docs

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

No Resource Locks at all docs

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

No Microsoft Defender for Cloud plans at all

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

No Microsoft Defender for Cloud plans at all

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

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

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

No PSRule for Azure results

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

No Storage Accounts found

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

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

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

No Virtual Networks

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

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

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

No Subnets

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

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

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

No Virtual Network Peerings

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

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

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

No Private Endpoints

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

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

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

Management Groups

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

No Management Groups configured for Diagnostic settings docs

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

All Management Groups are configured for Diagnostic settings docs

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

Subscriptions

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

No Subscriptions configured for Diagnostic settings docs

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

All Subscriptions are configured for Diagnostic settings docs

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

Resources

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

No Resources (1st party) Diagnostics capable

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

No ResourceDiagnostics Policy Lifecycle recommendations

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

No ResourceDiagnostics Policy Lifecycle recommendations

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

No ResourceDiagnostics Policy Lifecycle recommendations

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

Tenant

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

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

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

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

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

Management Groups

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

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

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

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

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

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

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

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

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

Subscriptions

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

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

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

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

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

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

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

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

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

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

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

No ServicePrincipals where the API returned 'Request_ResourceNotFound'

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

No Applications where the API returned 'Request_ResourceNotFound'

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

$servicePrincipalsOfTypeManagedIdentityCount AAD ServicePrincipals type=ManagedIdentity

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

$servicePrincipalsOfTypeApplicationCount AAD ServicePrincipals type=Application

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

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

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

$appsWithOtherOrgIdCount External (appOwnerOrganizationId) AAD ServicePrincipals type=Application

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

No information on Consumption

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

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

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

$customPolicyCreatedOrUpdatedCount Created/Updated custom Policy definitions

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

$customPolicySetCreatedOrUpdatedCount Created/Updated custom PolicySet definitions

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

$policyAssignmentsCreatedOrUpdatedCount Created/Updated Policy assignments

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

$customRoleDefinitionsCreatedOrUpdatedCount Created/Updated custom Role definitions

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

$customRoleDefinitionsCreatedOrUpdatedCount Created/Updated custom Role definitions

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

$resourcesCreatedOrChangedCount Created/Changed Resources

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

Policy $($namingPolicyCount) Naming findings

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

PolicySet $($namingPolicySetCount) Naming findings

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

Policy assignment $($namingPolicyAssignmentCount) Naming findings

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

Management Group $($namingManagementGroupCount) Naming findings

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

Subscription $($namingSubscriptionCount) Naming findings

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

RBAC $($namingRoleCount) Naming Findings

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

    Default Management Group docs

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

    Management Group Id: $mgId

    Management Group Path: $mgPath

    - -
    -"@ - } - foreach ($subEntry in $subscriptions | Sort-Object -Property subscription, subscriptionId) { - #$subPath = $htSubscriptionsMgPath.($subEntry.subscriptionId).pathDelimited - if ($subscriptionLinkedCount -gt 1) { - if (-not $NoScopeInsights) { - $script:html += @" - -
    -"@ - } - } - #exactly 1 - else { - if (-not $NoScopeInsights) { - $script:html += @" - $($subEntry.subscription -replace '<', '<' -replace '>', '>') ($($subEntry.subscriptionId)) -"@ - } - } - if (-not $NoScopeInsights) { - $script:html += @" - - -"@ - } - - if (-not $azAPICallConf['htParameters'].ManagementGroupsOnly) { - processScopeInsightsMgOrSub -mgOrSub 'sub' -subscriptionId $subEntry.subscriptionId -subscriptionsMgId $mgChild - } - - if (-not $NoScopeInsights) { - $script:html += @' -
    Highlight Subscription in HierarchyMap
    -'@ - } - if ($subscriptionLinkedCount -gt 1) { - if (-not $NoScopeInsights) { - $script:html += @' -
    -'@ - } - } - } - if (-not $NoScopeInsights) { - $script:html += @' -
    -'@ - } - - } - else { - if (-not $NoScopeInsights) { - $script:html += @" -
    - $subscriptionLinkedCount Subscriptions linked $subscriptionsOutOfScopelinkedDetail -"@ - } - } - if (-not $NoScopeInsights) { - $script:html += @' -
    -
    -'@ - } -} -#endregion HTML +. ".\$($ScriptPath)\functions\validateLeastPrivilegeForUser.ps1" +. ".\$($ScriptPath)\functions\getPolicyRemediation.ps1" +. ".\$($ScriptPath)\functions\getPolicyHash.ps1" +. ".\$($ScriptPath)\functions\detectPolicyEffect.ps1" +. ".\$($ScriptPath)\functions\exportResourceLocks.ps1" +. ".\$($ScriptPath)\functions\processHierarchyMapOnlyCustomData.ps1" +. ".\$($ScriptPath)\functions\processPrivateEndpoints.ps1" +. ".\$($ScriptPath)\functions\processNetwork.ps1" +. ".\$($ScriptPath)\functions\processStorageAccountAnalysis.ps1" +. ".\$($ScriptPath)\functions\processALZPolicyVersionChecker.ps1" +. ".\$($ScriptPath)\functions\getPIMEligible.ps1" +. ".\$($ScriptPath)\functions\testGuid.ps1" +. ".\$($ScriptPath)\functions\apiCallTracking.ps1" +. ".\$($ScriptPath)\functions\addRowToTable.ps1" +. ".\$($ScriptPath)\functions\testPowerShellVersion.ps1" +. ".\$($ScriptPath)\functions\setOutput.ps1" +. ".\$($ScriptPath)\functions\setTranscript.ps1" +. ".\$($ScriptPath)\functions\verifyModules3rd.ps1" +. ".\$($ScriptPath)\functions\checkAzGovVizVersion.ps1" +. ".\$($ScriptPath)\functions\handleCloudEnvironment.ps1" +. ".\$($ScriptPath)\functions\addHtParameters.ps1" +. ".\$($ScriptPath)\functions\selectMg.ps1" +. ".\$($ScriptPath)\functions\validateAccess.ps1" +. ".\$($ScriptPath)\functions\getEntities.ps1" +. ".\$($ScriptPath)\functions\setBaseVariablesMG.ps1" +. ".\$($ScriptPath)\functions\getTenantDetails.ps1" +. ".\$($ScriptPath)\functions\getDefaultManagementGroup.ps1" +. ".\$($ScriptPath)\functions\runInfo.ps1" +. ".\$($ScriptPath)\functions\processHierarchyMapOnly.ps1" +. ".\$($ScriptPath)\functions\getSubscriptions.ps1" +. ".\$($ScriptPath)\functions\detailSubscriptions.ps1" +. ".\$($ScriptPath)\functions\getOrphanedResources.ps1" +. ".\$($ScriptPath)\functions\getMDfCSecureScoreMG.ps1" +. ".\$($ScriptPath)\functions\getConsumption.ps1" +. ".\$($ScriptPath)\functions\cacheBuiltIn.ps1" +. ".\$($ScriptPath)\functions\prepareData.ps1" +. ".\$($ScriptPath)\functions\getGroupmembers.ps1" +. ".\$($ScriptPath)\functions\processAADGroups.ps1" +. ".\$($ScriptPath)\functions\processApplications.ps1" +. ".\$($ScriptPath)\functions\processManagedIdentities.ps1" +. ".\$($ScriptPath)\functions\createTagList.ps1" +. ".\$($ScriptPath)\functions\getResourceDiagnosticsCapability.ps1" +. ".\$($ScriptPath)\functions\getFileNaming.ps1" +. ".\$($ScriptPath)\functions\resolveObjectIds.ps1" +. ".\$($ScriptPath)\functions\namingValidation.ps1" +. ".\$($ScriptPath)\functions\removeInvalidFileNameChars.ps1" +. ".\$($ScriptPath)\functions\addIndexNumberToArray.ps1" +. ".\$($ScriptPath)\functions\processDiagramMermaid.ps1" +. ".\$($ScriptPath)\functions\buildMD.ps1" +. ".\$($ScriptPath)\functions\buildTree.ps1" +. ".\$($ScriptPath)\functions\buildJSON.ps1" +. ".\$($ScriptPath)\functions\buildPolicyAllJSON.ps1" +. ".\$($ScriptPath)\functions\stats.ps1" +#Region dataCollectionFunctions +. ".\$($ScriptPath)\functions\dataCollection\dataCollectionFunctions.ps1" +. ".\$($ScriptPath)\functions\processDataCollection.ps1" +. ".\$($ScriptPath)\functions\exportBaseCSV.ps1" +. ".\$($ScriptPath)\functions\html\htmlFunctions.ps1" +. ".\$($ScriptPath)\functions\processTenantSummary.ps1" +. ".\$($ScriptPath)\functions\processDefinitionInsights.ps1" +. ".\$($ScriptPath)\functions\processScopeInsightsMgOrSub.ps1" +. ".\$($ScriptPath)\functions\showMemoryUsage.ps1" +#EndRegion dataCollectionFunctions #endregion Functions $funcAddRowToTable = $function:addRowToTable.ToString() diff --git a/setup.md b/setup.md index ff60d736..7f8694fd 100644 --- a/setup.md +++ b/setup.md @@ -5,6 +5,7 @@ Follow these steps to deploy the Azure Governance Visualizer. There are three se - Running it ad-hoc from a workstation console or dev container - Running it from Azure DevOps - Running it from GitHub +- Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App No matter which of the three you choose, they all evaluate the same governance concerns and produce the same reporting results, just the execution and reporting environment is distinct. Use whichever environment is best suited for your situation. @@ -17,16 +18,22 @@ No matter which of the three you choose, they all evaluate the same governance c To set up local execution of the Azure Governance Visualizer without involving automation from Azure pipelines or GitHub actions. This solution is good for proof of value exploration, local development, etc. It's encouraged that you use Azure DevOps pipelines or GitHub actions for a formal deployment. -:arrow_right: Follow the instructions to [Configure and run from the console](./run-from/console.md). +:arrow_right: Follow the instructions to [Configure and run from the console](./setup/console.md). ## Set up and run Azure Governance Visualizer in Azure DevOps The Azure Governance Visualizer lifecycle can be hosted out of Azure DevOps. This includes automated pipelines, service connections, and even automated wiki generations. This path also optionally supports publishing the generated HTML report to Azure Web Apps. -:arrow_right: Follow the instructions to [Configure and run from Azure DevOps](./run-from/azure-devops.md). +:arrow_right: Follow the instructions to [Configure and run from Azure DevOps](./setup/azure-devops.md). ## Set up and run Azure Governance Visualizer in GitHub To set up the Azure Governance Visualizer lifecycle, including automated actions, service connections, and GitHub Codespaces. This path also optionally supports publishing the generated HTML report to Azure Web Apps. -:arrow_right: Follow the instructions to [Configure and run from GitHub](./run-from/github.md). +:arrow_right: Follow the instructions to [Configure and run from GitHub](./setup/github.md). + +## Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App + +Set up the Azure Web App, so that with each execution of the Azure Governance Visualizer the latest HTML file gets published to the azure Web App. Supported setups are Azure DevOps and GitHub Actions. + +:arrow_right: Follow the instructions to [Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App](./run-from/azure-web-app.md). diff --git a/run-from/azure-devops.md b/setup/azure-devops.md similarity index 100% rename from run-from/azure-devops.md rename to setup/azure-devops.md diff --git a/setup/azure-web-app.md b/setup/azure-web-app.md new file mode 100644 index 00000000..60909590 --- /dev/null +++ b/setup/azure-web-app.md @@ -0,0 +1,34 @@ +# 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. + +You can either setup the azure Web App [manually](#manual-setup) or deploy [code based](#code-based-setup) using the Azure Governance Visualizer accelerator. + +## Manual setup + +### Prerequisites +* Deploy a simple webapp on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform +![alt text](../img/webapp_create.png "Azure Web App Create") +* Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on +![alt text](../img/webapp_configure.png "Azure Web App Configure") +* No need to configure anything, unless your organization policies require you to do so +NOTE: it is a good practice to tag your resource for operational and finance reasons +* In the webapp _Configuration_ add the name of the HTML output file to the _Default Documents_ +![alt text](../img/webapp_defaultdocs.png "Azure Web App Default documents") +* Make sure to configure Authentication! +![alt text](../img/webapp_authentication.png "Azure Web App Authentication") + +### Azure DevOps + +* Assign the Azure DevOps Service Connection´s Service Principal with RBAC Role __Website Contributor__ on the Azure Web App +* Edit the `.azuredevops/AzGovViz.variables.yml` file +![alt text](../img/webapp_AzDO_yml.png "Azure DevOps YAML variables") + +### GitHub Actions + +* Assign the Service Principal used in GitHub with RBAC Role __Website Contributor__ on the Azure Web App +* Edit the `.github/workflows/AzGovViz_OIDC.yml` or `.github/workflows/AzGovViz.yml` file +![alt text](../img/webapp_GitHub_yml.png "GitHub YAML variables") + +## Code based setup +Use the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) to deploy the Azure Web App per code. \ No newline at end of file diff --git a/run-from/console.md b/setup/console.md similarity index 100% rename from run-from/console.md rename to setup/console.md diff --git a/run-from/github.md b/setup/github.md similarity index 100% rename from run-from/github.md rename to setup/github.md diff --git a/version.json b/version.json index 441a8e59..a9a93cc6 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "ProductVersion": "6.3.7" + "ProductVersion": "6.3.71" } \ No newline at end of file From 95173d2f77dec9a1deb9e3570ec93ebc218befd1 Mon Sep 17 00:00:00 2001 From: Julian Hayward Date: Mon, 5 Feb 2024 12:40:49 +0100 Subject: [PATCH 15/18] fix link, typ0 --- setup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.md b/setup.md index 7f8694fd..cca9fa5a 100644 --- a/setup.md +++ b/setup.md @@ -34,6 +34,6 @@ To set up the Azure Governance Visualizer lifecycle, including automated actions ## Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App -Set up the Azure Web App, so that with each execution of the Azure Governance Visualizer the latest HTML file gets published to the azure Web App. Supported setups are Azure DevOps and GitHub Actions. +Set up the Azure Web App, so that with each execution of the Azure Governance Visualizer the latest HTML file gets published to an Azure Web App. Supported setups are Azure DevOps and GitHub Actions. -:arrow_right: Follow the instructions to [Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App](./run-from/azure-web-app.md). +:arrow_right: Follow the instructions to [Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App](./setup/azure-web-app.md). From 2f22aaa24647b6ebb1a041c4bbb12b176c0c1dae Mon Sep 17 00:00:00 2001 From: Julian Hayward Date: Mon, 5 Feb 2024 12:43:16 +0100 Subject: [PATCH 16/18] style fix --- setup/azure-web-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/azure-web-app.md b/setup/azure-web-app.md index 60909590..2d34a280 100644 --- a/setup/azure-web-app.md +++ b/setup/azure-web-app.md @@ -7,7 +7,7 @@ You can either setup the azure Web App [manually](#manual-setup) or deploy [code ## Manual setup ### Prerequisites -* Deploy a simple webapp on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform +* Deploy a Web App on Azure. This can be the smallest SKU or a FREE SKU. It doesn't matter whether you choose Windows or Linux as the platform ![alt text](../img/webapp_create.png "Azure Web App Create") * Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on ![alt text](../img/webapp_configure.png "Azure Web App Configure") From 10fd18dc105b4da5f5d3a1112788f3ea75b19885 Mon Sep 17 00:00:00 2001 From: Julian Hayward Date: Tue, 6 Feb 2024 21:30:15 +0100 Subject: [PATCH 17/18] 6.4.0 --- README.md | 13 +- history.md | 11 + pwsh/AzGovVizParallel.ps1 | 2184 +++++++++-------- pwsh/dev/devAzGovVizParallel.ps1 | 95 +- pwsh/dev/functions/buildJSON.ps1 | 74 +- pwsh/dev/functions/buildTree.ps1 | 96 +- pwsh/dev/functions/cacheBuiltIn.ps1 | 18 +- .../dataCollectionFunctions.ps1 | 113 +- pwsh/dev/functions/detectPolicyEffect.ps1 | 4 +- pwsh/dev/functions/getMDfCSecureScoreMG.ps1 | 27 +- pwsh/dev/functions/getPIMEligible.ps1 | 257 +- .../getResourceDiagnosticsCapability.ps1 | 4 +- pwsh/dev/functions/html/htmlFunctions.ps1 | 2 +- pwsh/dev/functions/namingValidation.ps1 | 8 +- pwsh/dev/functions/processAADGroups.ps1 | 65 +- pwsh/dev/functions/processApplications.ps1 | 143 +- pwsh/dev/functions/processDataCollection.ps1 | 466 ++-- .../functions/processDefinitionInsights.ps1 | 12 +- pwsh/dev/functions/processNetwork.ps1 | 6 +- .../dev/functions/processPrivateEndpoints.ps1 | 12 +- .../functions/processScopeInsightsMgOrSub.ps1 | 34 +- .../processStorageAccountAnalysis.ps1 | 514 ++-- pwsh/dev/functions/processTenantSummary.ps1 | 238 +- version.json | 2 +- 24 files changed, 2406 insertions(+), 1992 deletions(-) diff --git a/README.md b/README.md index 510bf8c8..4b672b0f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ From the collected data it generates enriched insights for capabilities such as Within an HTML output it provides visibility on your __HierarchyMap__, creates a __TenantSummary__, creates __DefinitionInsights__ and builds granular __ScopeInsights__ on Azure Management Groups and Subscriptions. -Further, CSV exports with enriched information per capability will be generated and detailed JSON files are exported which document your entire Azure tenant setup for Management Groups, Subscriptions, Azure RBAC definitions and assignments, Azure policy definitions and assignments. These exports come in handy for change tracking scenarios as well as redeployment of configuration (e.g. tenant migration scenrio) and can even serve as a backup. +Further, CSV exports with enriched information per capability will be generated and detailed JSON files are exported which document your entire Azure tenant setup for Management Groups, Subscriptions, Azure RBAC definitions and assignments, Azure policy definitions and assignments. These exports come in handy for change tracking scenarios as well as redeployment of configuration (e.g. tenant migration scenario) and can even serve as a backup. The technical requirements as well as the required permissions are minimal. @@ -87,9 +87,16 @@ As an alternative, you can use the [Azure Governance Visualizer accelerator](htt ## Release history -__Changes__ (2024-Jan-08 / 6.3.7 Minor) +__Changes__ (2024-Feb-06 / 6.4.0 Minor) -* fix: Ignore `ARMLocation` in case not Public Cloud (AzureCloud) +* change PowerShell parallel handling / batches +* add addition JSON outputs 'definitions_tracking' and 'assignments_tracking' (JSON filenames have no displayName included; GUIDs only) +* update ARM API-version for RBAC Role definitions. Using `2022-05-01-preview` instead of `2018-11-01-preview` consequently +* fix *_roleDefinitions.csv - description partially missing +* optimize array handling / best practices +* optimize getting private endpoint capacle resource types / in case resource provider 'microsoft.network' is not registered, try with next available subscription instead of throwing +* use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.2.0 +* documentation update - style guidance, links updates - kudos @ckittel [Full release history](history.md) diff --git a/history.md b/history.md index bb34180d..cfe2dfdf 100644 --- a/history.md +++ b/history.md @@ -4,6 +4,17 @@ ### Azure Governance Visualizer version 6 +__Changes__ (2024-Feb-06 / 6.4.0 Minor) + +* change PowerShell parallel handling / batches +* add addition JSON outputs 'definitions_tracking' and 'assignments_tracking' (JSON filenames have no displayName included; GUIDs only) +* update ARM API-version for RBAC Role definitions. Using `2022-05-01-preview` instead of `2018-11-01-preview` consequently +* fix *_roleDefinitions.csv - description partially missing +* optimize array handling / best practices +* optimize getting private endpoint capacle resource types / in case resource provider 'microsoft.network' is not registered, try with next available subscription instead of throwing +* use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.2.0 +* documentation update - style guidance, links updates - kudos @ckittel + __Changes__ (2024-Jan-08 / 6.3.7 Minor) * fix: Ignore `ARMLocation` in case not Public Cloud (AzureCloud) diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 9ea96a6c..41bfaa31 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -365,14 +365,14 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.3.71', + $ProductVersion = '6.4.0', [string] $GithubRepository = 'aka.ms/AzGovViz', # <--- AzAPICall related parameters #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall [string] - $AzAPICallVersion = '1.1.85', + $AzAPICallVersion = '1.2.0', [switch] $DebugAzAPICall, @@ -951,9 +951,11 @@ function buildJSON { $htSubRGPolicyAssignments.($subId) = @{} } if (-not $htSubRGPolicyAssignments.($subId).PolicyAssignments) { - $htSubRGPolicyAssignments.($subId).PolicyAssignments = @() + $htSubRGPolicyAssignments.($subId).PolicyAssignments = [System.Collections.ArrayList]@() + } + foreach ($rgpafg in $rgpa.group) { + $null = $htSubRGPolicyAssignments.($subId).PolicyAssignments.Add($rgpafg) } - $htSubRGPolicyAssignments.($subId).PolicyAssignments += $rgpa.group } } } @@ -973,9 +975,11 @@ function buildJSON { $htSubRGRoleAssignments.($subId) = @{} } if (-not $htSubRGRoleAssignments.($subId).RoleAssignments) { - $htSubRGRoleAssignments.($subId).RoleAssignments = @() + $htSubRGRoleAssignments.($subId).RoleAssignments = [System.Collections.ArrayList]@() + } + foreach ($rgrafg in $rgra.group) { + $null = $htSubRGRoleAssignments.($subId).RoleAssignments.Add($rgrafg) } - $htSubRGRoleAssignments.($subId).RoleAssignments += $rgra.group } #res @@ -1226,6 +1230,9 @@ function buildJSON { } if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)") { + if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Definitions") { + $createDefinitionsLegacyAndNew = $true + } Write-Host ' Cleaning old state (Pipeline only)' Remove-Item -Recurse -Force "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)" } @@ -1247,6 +1254,7 @@ function buildJSON { } $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Definitions" -ItemType directory -Path $outputPath + $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking" -ItemType directory -Path $outputPath @@ -1260,16 +1268,47 @@ function buildJSON { $null = New-Item -Name "$($pathRoleDefinitionCustom)" -ItemType directory -Path $outputPath $null = New-Item -Name "$($pathRoleDefinitionBuiltIn)" -ItemType directory -Path $outputPath } + $pathRoleDefinitionsTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)RoleDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionsTracking)")) { + $null = New-Item -Name $pathRoleDefinitionsTracking -ItemType directory -Path $outputPath + $pathRoleDefinitionCustomTracking = "$($pathRoleDefinitionsTracking)$($DirectorySeparatorChar)Custom" + $pathRoleDefinitionBuiltInTracking = "$($pathRoleDefinitionsTracking)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathRoleDefinitionCustomTracking)" -ItemType directory -Path $outputPath + $null = New-Item -Name "$($pathRoleDefinitionBuiltInTracking)" -ItemType directory -Path $outputPath + } if (($htCacheDefinitionsRole).Keys.Count -gt 0) { foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { ($htCacheDefinitionsRole).($_).IsCustom }) | Sort-Object) { $htJSON.RoleDefinitions.($roleDefinition) = ($htCacheDefinitionsRole).($roleDefinition).Json.properties $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionCustom)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 + + #if a custom role has multiple assignable scopes, the definition id may vary depending which scope AzGovViz retrieved the definition from, therefore for better change tracking we pack assignablescopes, sort them and use the first entry as id + + if (($htCacheDefinitionsRole).($roleDefinition).Json.properties.assignableScopes.Count -gt 1) { + $jsonAdjustment4Tracking = (($htCacheDefinitionsRole).($roleDefinition).Json).psobject.copy() + $arrayAssignableScopes = [System.Collections.ArrayList]@() + foreach ($assignableScope in $jsonAdjustment4Tracking.properties.assignableScopes) { + if ($assignableScope -like '/subscriptions/*') { + $null = $arrayAssignableScopes.Add("$($assignableScope)/providers/Microsoft.Authorization/roleDefinitions/$($jsonAdjustment4Tracking.name)") + } + else { + $null = $arrayAssignableScopes.Add("/providers/Microsoft.Authorization/roleDefinitions/$($jsonAdjustment4Tracking.name)") + } + } + $jsonAdjustment4Tracking.id = ($arrayAssignableScopes | Sort-Object)[0] + $jsonConvertedTracking = $jsonAdjustment4Tracking | ConvertTo-Json -Depth 99 + } + else { + $jsonConvertedTracking = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionCustomTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsRole).($roleDefinition).Id).json" -Encoding utf8 } foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { -not ($htCacheDefinitionsRole).($_).IsCustom })) { - $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name ) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 + $jsonConvertedTracking = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionBuiltInTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsRole).($roleDefinition).Id).json" -Encoding utf8 } } @@ -1279,10 +1318,18 @@ function buildJSON { $pathPolicyDefinitionBuiltIn = "$($pathPolicyDefinitions)$($DirectorySeparatorChar)BuiltIn" $null = New-Item -Name "$($pathPolicyDefinitionBuiltIn)" -ItemType directory -Path $outputPath } + $pathPolicyDefinitionsTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicyDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionsTracking)")) { + $null = New-Item -Name $pathPolicyDefinitionsTracking -ItemType directory -Path $outputPath + $pathPolicyDefinitionBuiltInTracking = "$($pathPolicyDefinitionsTracking)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathPolicyDefinitionBuiltInTracking)" -ItemType directory -Path $outputPath + } if (($htCacheDefinitionsPolicy).Keys.Count -gt 0) { foreach ($policyDefinition in ($htCacheDefinitionsPolicy).Keys.where( { ($htCacheDefinitionsPolicy).($_).Type -eq 'BuiltIn' })) { $jsonConverted = ($htCacheDefinitionsPolicy).($policyDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicy).($policyDefinition).displayName) ($(($htCacheDefinitionsPolicy).($policyDefinition).Json.name)).json" -Encoding utf8 + $jsonConvertedTracking = ($htCacheDefinitionsPolicy).($policyDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionBuiltInTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsPolicy).($policyDefinition).Json.name).json" -Encoding utf8 } } @@ -1292,10 +1339,18 @@ function buildJSON { $pathPolicySetDefinitionBuiltIn = "$($pathPolicySetDefinitions)$($DirectorySeparatorChar)BuiltIn" $null = New-Item -Name "$($pathPolicySetDefinitionBuiltIn)" -ItemType directory -Path $outputPath } + $pathPolicySetDefinitionsTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicySetDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionsTracking)")) { + $null = New-Item -Name $pathPolicySetDefinitionsTracking -ItemType directory -Path $outputPath + $pathPolicySetDefinitionBuiltInTracking = "$($pathPolicySetDefinitionsTracking)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathPolicySetDefinitionBuiltInTracking)" -ItemType directory -Path $outputPath + } if (($htCacheDefinitionsPolicySet).Keys.Count -gt 0) { foreach ($policySetDefinition in ($htCacheDefinitionsPolicySet).Keys.where( { ($htCacheDefinitionsPolicySet).($_).Type -eq 'BuiltIn' })) { $jsonConverted = ($htCacheDefinitionsPolicySet).($policySetDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicySet).($policySetDefinition).displayName) ($(($htCacheDefinitionsPolicySet).($policySetDefinition).Json.name)).json" -Encoding utf8 + $jsonConverted = ($htCacheDefinitionsPolicySet).($policySetDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionBuiltInTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsPolicySet).($policySetDefinition).Json.name).json" -Encoding utf8 } } @@ -1330,6 +1385,12 @@ function buildJSON { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Tenant" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 } $htTree.'Tenant'.'ManagementGroups' = [ordered] @{} @@ -1338,6 +1399,9 @@ function buildJSON { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Assignments")) { $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Assignments" -ItemType directory -Path $outputPath } + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking")) { + $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking" -ItemType directory -Path $outputPath + } buildTree -mgId $ManagementGroupId -json $json -prnt "$($JSONPath)$($DirectorySeparatorChar)Tenant" @@ -1592,12 +1656,13 @@ function buildTree($mgId, $prnt) { $json.'ManagementGroups' = [ordered]@{} } $json = $json.'ManagementGroups'.($getMg.Id) = [ordered]@{} - foreach ($mgCap in $htJSON.ManagementGroups.($getMg.Id).keys) { - $json.$mgCap = $htJSON.ManagementGroups.($getMg.Id).$mgCap + $mgJson = $htJSON.ManagementGroups.($getMg.Id) + foreach ($mgCap in $mgJson.keys) { + $json.$mgCap = $mgJson.$mgCap if ($mgCap -eq 'PolicyDefinitionsCustom') { $mgCapShort = 'pd' - foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pdc) + foreach ($pdc in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($pdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -1611,12 +1676,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($mgCap -eq 'PolicySetDefinitionsCustom') { $mgCapShort = 'psd' - foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($psdc) + foreach ($psdc in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($psdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -1630,12 +1702,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($mgCap -eq 'PolicyAssignments') { $mgCapShort = 'pa' - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pa) + foreach ($pa in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($pa) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -1648,15 +1727,20 @@ function buildTree($mgId, $prnt) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking$($DirectorySeparatorChar)$($mgCap)$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } #marker if ($mgCap -eq 'RoleAssignments') { $mgCapShort = 'ra' - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($ra) + foreach ($ra in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } @@ -1674,15 +1758,15 @@ function buildTree($mgId, $prnt) { } if ($mgCap -eq 'Subscriptions') { - foreach ($sub in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $subNameValid = removeInvalidFileNameChars $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).SubscriptionName + foreach ($sub in $mgJson.($mgCap).Keys) { + $subNameValid = removeInvalidFileNameChars $mgJson.($mgCap).($sub).SubscriptionName $subFolderName = "$($prntx)$($DirectorySeparatorChar)$($subNameValid) ($($sub))" $null = New-Item -Name $subFolderName -ItemType directory -Path $outputPath - foreach ($subCap in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).Keys) { + foreach ($subCap in $mgJson.($mgCap).($sub).Keys) { if ($subCap -eq 'PolicyDefinitionsCustom') { $subCapShort = 'pd' - foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pdc) + foreach ($pdc in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($pdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -1696,12 +1780,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($sub)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($subCap -eq 'PolicySetDefinitionsCustom') { $subCapShort = 'psd' - foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($psdc) + foreach ($psdc in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($psdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -1715,12 +1806,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($sub)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($subCap -eq 'PolicyAssignments') { $subCapShort = 'pa' - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pa) + foreach ($pa in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($pa) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -1739,8 +1837,8 @@ function buildTree($mgId, $prnt) { #marker if ($subCap -eq 'RoleAssignments') { $subCapShort = 'ra' - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($ra) + foreach ($ra in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } @@ -1761,12 +1859,12 @@ function buildTree($mgId, $prnt) { if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { if (-not $JsonExportExcludeResourceGroups) { if ($subCap -eq 'ResourceGroups') { - foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { + foreach ($rg in $mgJson.($mgCap).($sub).($subCap).Keys | Sort-Object) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" } - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.($pa) + foreach ($pa in $mgJson.($mgCap).($sub).($subCap).($rg).PolicyAssignments.keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($rg).PolicyAssignments.($pa) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -1791,12 +1889,12 @@ function buildTree($mgId, $prnt) { if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { if (-not $JsonExportExcludeResourceGroups) { if ($subCap -eq 'ResourceGroups') { - foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { + foreach ($rg in $mgJson.($mgCap).($sub).($subCap).Keys | Sort-Object) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" } - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.($ra) + foreach ($ra in $mgJson.($mgCap).($sub).($subCap).($rg).RoleAssignments.keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($rg).RoleAssignments.($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } @@ -1814,12 +1912,12 @@ function buildTree($mgId, $prnt) { #res if (-not $JsonExportExcludeResources) { - foreach ($res in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.keys) { + foreach ($res in $mgJson.($mgCap).($sub).($subCap).($rg).Resources.keys) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)")) { $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)" -ItemType directory -Path "$($outputPath)" } - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.($ra) + foreach ($ra in $mgJson.($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } @@ -1935,12 +2033,11 @@ function cacheBuiltIn { foreach ($roledefinitionId in $builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$builtinPolicyDefinition.Id + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [System.Collections.ArrayList]@() + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($builtinPolicyDefinition.Id) } else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $builtinPolicyDefinition.Id - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($builtinPolicyDefinition.Id) } } } @@ -2024,12 +2121,11 @@ function cacheBuiltIn { foreach ($roledefinitionId in $staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$staticPolicyDefinition.Id + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [System.Collections.ArrayList]@() + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($staticPolicyDefinition.Id) } else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $staticPolicyDefinition.Id - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($staticPolicyDefinition.Id) } } } @@ -2099,13 +2195,11 @@ function cacheBuiltIn { $currentTask = 'Caching built-in Role definitions' Write-Host " $currentTask" $uri = "$($azAPICallConf['azAPIEndpointUrls'].'ARM')/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" } else { $currentTask = "Caching built-in Role definitions (Location: '$($ARMLocation)')" Write-Host " $currentTask" $uri = "$($azAPICallConf['azAPIEndpointUrls']."ARM$($ARMLocation)")/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" } $method = 'GET' @@ -2145,7 +2239,7 @@ function cacheBuiltIn { ($script:htCacheDefinitionsRole).($roleDefinition.name).NotActions = ($roleDefinition.properties.permissions.notActions) ($script:htCacheDefinitionsRole).($roleDefinition.name).DataActions = ($roleDefinition.properties.permissions.dataActions) ($script:htCacheDefinitionsRole).($roleDefinition.name).NotDataActions = ($roleDefinition.properties.permissions.notDataActions) - ($script:htCacheDefinitionsRole).($roleDefinition.name).Json = ($roleDefinition.properties) + ($script:htCacheDefinitionsRole).($roleDefinition.name).Json = $roleDefinition ($script:htCacheDefinitionsRole).($roleDefinition.name).LinkToAzAdvertizer = "$($roleDefinition.properties.roleName)" ($script:htCacheDefinitionsRole).($roleDefinition.name).RoleCanDoRoleAssignments = $roleCapable4RoleAssignmentsWrite } @@ -2409,10 +2503,10 @@ function detectPolicyEffect { 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 = @() + $arrayAllowed = [System.Collections.ArrayList]@() foreach ($allowedValue in $policyDefinition.properties.parameters.($Match.Value).allowedValues) { if ($allowedValue -in $ValidPolicyEffects) { - $arrayAllowed += $allowedValue + $null = $arrayAllowed.Add($allowedValue) } else { Write-Host "invalid allowedValue effect $($allowedValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" @@ -3496,25 +3590,22 @@ function getMDfCSecureScoreMG { } "@ - $getMgAscSecureScore = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' - + $getMgAscSecureScore = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' -unhandledErrorAction ContinueQuiet if ($getMgAscSecureScore) { - if ($getMgAscSecureScore -eq 'capitulation') { - Write-Host ' Microsoft Defender for Cloud SecureScore for Management Groups will not be available' -ForegroundColor Yellow - } - else { - Write-Host " Retrieved 'Microsoft Defender for Cloud' SecureScore for $($getMgAscSecureScore.Count) Management Groups" - foreach ($entry in $getMgAscSecureScore) { - $script:htMgASCSecureScore.($entry.mgId) = @{} - if ($entry.secureScore -eq 404) { - $script:htMgASCSecureScore.($entry.mgId).SecureScore = 'n/a' - } - else { - $script:htMgASCSecureScore.($entry.mgId).SecureScore = $entry.secureScore - } + Write-Host " Retrieved 'Microsoft Defender for Cloud' SecureScore for $($getMgAscSecureScore.Count) Management Groups" + foreach ($entry in $getMgAscSecureScore) { + $script:htMgASCSecureScore.($entry.mgId) = @{} + if ($entry.secureScore -eq 404) { + $script:htMgASCSecureScore.($entry.mgId).SecureScore = 'n/a' + } + else { + $script:htMgASCSecureScore.($entry.mgId).SecureScore = $entry.secureScore } } } + else { + Write-Host ' Microsoft Defender for Cloud SecureScore for Management Groups will not be available' -ForegroundColor Yellow + } $end = Get-Date Write-Host "Getting Microsoft Defender for Cloud Secure Score for Management Groups duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" @@ -3773,143 +3864,156 @@ function getPIMEligible { $htPIMEligibleDirect = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} $relevantSubscriptionIds = $subsToProcessInCustomDataCollection.subscriptionId - $scopesToIterate | ForEach-Object -Parallel { - $scope = $_ - $azAPICallConf = $using:azAPICallConf - $arrayPIMEligible = $using:arrayPIMEligible - $htPIMEligibleDirect = $using:htPIMEligibleDirect - if ($scope.type -eq 'managementgroup') { $htManagementGroupsMgPath = $using:htManagementGroupsMgPath } - if ($scope.type -eq 'subscription') { $htSubscriptionsMgPath = $using:htSubscriptionsMgPath } - $htPrincipals = $using:htPrincipals - $htUserTypesGuest = $using:htUserTypesGuest - $htServicePrincipals = $using:htServicePrincipals - $relevantSubscriptionIds = $using:relevantSubscriptionIds - $function:resolveObjectIds = $using:funcResolveObjectIds - $function:testGuid = $using:funcTestGuid - $processThisScope = $true - if ($scope.type -eq 'subscription') { - if (($scope.externalId -replace '.*/') -notin $relevantSubscriptionIds) { - Write-Host " Non relevant subscriptionId '$(($scope.externalId -replace '.*/'))' /skipping this subscription as it is not contained in the 'Relevant Subscriptions' collection (needs investigation)" -ForegroundColor DarkRed - $processThisScope = $false - } - } + if ($scopesToIterate.Count -gt 0) { - if ($processThisScope -eq $true) { - $currentTask = "Get Eligible assignments for Scope $($scope.type): $($scope.externalId -replace '.*/')" - $extUri = "?`$expand=linkedEligibleRoleAssignment,subject,roleDefinition(`$expand=resource)&`$count=true&`$filter=(roleDefinition/resource/id eq '$($scope.id)')+and+(assignmentState eq 'Eligible')&`$top=100" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/roleAssignments" + $extUri - $resx = AzAPICall -AzAPICallConfiguration $azapicallConf -currentTask $currentTask -uri $uri + $batchSize = [math]::ceiling($scopesToIterate.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $scopesToIterateBatch = ($scopesToIterate) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($scopesToIterateBatch.Count) batches" + + $scopesToIterateBatch | ForEach-Object -Parallel { + $scope = $_ + $azAPICallConf = $using:azAPICallConf + $arrayPIMEligible = $using:arrayPIMEligible + $htPIMEligibleDirect = $using:htPIMEligibleDirect + $htPrincipals = $using:htPrincipals + $htUserTypesGuest = $using:htUserTypesGuest + $htServicePrincipals = $using:htServicePrincipals + $relevantSubscriptionIds = $using:relevantSubscriptionIds + $function:resolveObjectIds = $using:funcResolveObjectIds + $function:testGuid = $using:funcTestGuid - if ($resx.Count -gt 0) { + foreach ($scope in $_.Group) { + if ($scope.type -eq 'managementgroup') { $htManagementGroupsMgPath = $using:htManagementGroupsMgPath } + if ($scope.type -eq 'subscription') { $htSubscriptionsMgPath = $using:htSubscriptionsMgPath } - $users = $resx.where({ $_.subject.type -eq 'user' }) - if ($users.Count -gt 0) { - ResolveObjectIds -objectIds $users.subject.id -showActivity + $processThisScope = $true + if ($scope.type -eq 'subscription') { + if (($scope.externalId -replace '.*/') -notin $relevantSubscriptionIds) { + Write-Host " Non relevant subscriptionId '$(($scope.externalId -replace '.*/'))' /skipping this subscription as it is not contained in the 'Relevant Subscriptions' collection (needs investigation)" -ForegroundColor DarkRed + $processThisScope = $false + } } - foreach ($entry in $resx) { - $scopeId = $scope.externalId -replace '.*/' - if ($scope.type -eq 'managementgroup') { - $ScopeType = 'MG' - $ManagementGroupId = $scopeId - $SubscriptionId = '' - $SubscriptionDisplayName = '' - if ($htManagementGroupsMgPath.($scopeId)) { - $MgDetails = $htManagementGroupsMgPath.($scopeId) - $ManagementGroupDisplayName = $MgDetails.DisplayName - $ScopeDisplayName = $MgDetails.DisplayName - $MgPath = $MgDetails.path - $MgLevel = $MgDetails.level - } - else { - $ManagementGroupDisplayName = 'notAccessible' - $ScopeDisplayName = 'notAccessible' - $MgPath = 'notAccessible' - $MgLevel = 'notAccessible' + if ($processThisScope -eq $true) { + $currentTask = "Get Eligible assignments for Scope $($scope.type): $($scope.externalId -replace '.*/')" + $extUri = "?`$expand=linkedEligibleRoleAssignment,subject,roleDefinition(`$expand=resource)&`$count=true&`$filter=(roleDefinition/resource/id eq '$($scope.id)')+and+(assignmentState eq 'Eligible')&`$top=100" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/roleAssignments" + $extUri + $resx = AzAPICall -AzAPICallConfiguration $azapicallConf -currentTask $currentTask -uri $uri + + if ($resx.Count -gt 0) { + + $users = $resx.where({ $_.subject.type -eq 'user' }) + if ($users.Count -gt 0) { + ResolveObjectIds -objectIds $users.subject.id -showActivity } - if ($entry.memberType -eq 'direct') { - $script:htPIMEligibleDirect.($entry.id) = @{} - $script:htPIMEligibleDirect.($entry.id).clear = $scopeId - if ($scopeId -eq $ManagementGroupDisplayName) { - $script:htPIMEligibleDirect.($entry.id).enriched = "$($scopeId) [Level $($MgLevel)]" + foreach ($entry in $resx) { + $scopeId = $scope.externalId -replace '.*/' + if ($scope.type -eq 'managementgroup') { + $ScopeType = 'MG' + $ManagementGroupId = $scopeId + $SubscriptionId = '' + $SubscriptionDisplayName = '' + if ($htManagementGroupsMgPath.($scopeId)) { + $MgDetails = $htManagementGroupsMgPath.($scopeId) + $ManagementGroupDisplayName = $MgDetails.DisplayName + $ScopeDisplayName = $MgDetails.DisplayName + $MgPath = $MgDetails.path + $MgLevel = $MgDetails.level + } + else { + $ManagementGroupDisplayName = 'notAccessible' + $ScopeDisplayName = 'notAccessible' + $MgPath = 'notAccessible' + $MgLevel = 'notAccessible' + } + + if ($entry.memberType -eq 'direct') { + $script:htPIMEligibleDirect.($entry.id) = @{} + $script:htPIMEligibleDirect.($entry.id).clear = $scopeId + if ($scopeId -eq $ManagementGroupDisplayName) { + $script:htPIMEligibleDirect.($entry.id).enriched = "$($scopeId) [Level $($MgLevel)]" + } + else { + $script:htPIMEligibleDirect.($entry.id).enriched = "$($ManagementGroupDisplayName) ($($scopeId)) [Level $($MgLevel)]" + } + } } - else { - $script:htPIMEligibleDirect.($entry.id).enriched = "$($ManagementGroupDisplayName) ($($scopeId)) [Level $($MgLevel)]" + if ($scope.type -eq 'subscription') { + $ScopeType = 'Sub' + #$ManagementGroupId = '' + $SubscriptionId = $scopeId + if ($htSubscriptionsMgPath.($scopeId)) { + $MgDetails = $htSubscriptionsMgPath.($scopeId) + $SubscriptionDisplayName = $MgDetails.DisplayName + $ScopeDisplayName = $MgDetails.DisplayName + $MgPath = $MgDetails.path + $MgLevel = $MgDetails.level + $ManagementGroupId = $MgDetails.Parent + $ManagementGroupDisplayName = $MgDetails.ParentName + } + else { + $SubscriptionDisplayName = 'notAccessible' + $ScopeDisplayName = 'notAccessible' + $MgPath = 'notAccessible' + $MgLevel = 'notAccessible' + } + #$ManagementGroupDisplayName = '' + } - } - } - if ($scope.type -eq 'subscription') { - $ScopeType = 'Sub' - #$ManagementGroupId = '' - $SubscriptionId = $scopeId - if ($htSubscriptionsMgPath.($scopeId)) { - $MgDetails = $htSubscriptionsMgPath.($scopeId) - $SubscriptionDisplayName = $MgDetails.DisplayName - $ScopeDisplayName = $MgDetails.DisplayName - $MgPath = $MgDetails.path - $MgLevel = $MgDetails.level - $ManagementGroupId = $MgDetails.Parent - $ManagementGroupDisplayName = $MgDetails.ParentName - } - else { - $SubscriptionDisplayName = 'notAccessible' - $ScopeDisplayName = 'notAccessible' - $MgPath = 'notAccessible' - $MgLevel = 'notAccessible' - } - #$ManagementGroupDisplayName = '' - } + if ($entry.subject.type -eq 'user') { + if ($htPrincipals.($entry.subject.id)) { + $userDetail = $htPrincipals.($entry.subject.id) + $principalType = "$($userDetail.type) $($userDetail.userType)" + } + else { + $principalType = $entry.subject.type + } + } + else { + $principalType = $entry.subject.type + } - if ($entry.subject.type -eq 'user') { - if ($htPrincipals.($entry.subject.id)) { - $userDetail = $htPrincipals.($entry.subject.id) - $principalType = "$($userDetail.type) $($userDetail.userType)" - } - else { - $principalType = $entry.subject.type + $roleType = 'undefined' + if ($entry.roleDefinition.type -eq 'BuiltInRole') { $roleType = 'Builtin' } + if ($entry.roleDefinition.type -eq 'CustomRole') { $roleType = 'Custom' } + + $null = $script:arrayPIMEligible.Add([PSCustomObject]@{ + ScopeType = $ScopeType + ScopeId = $scopeId + ScopeDisplayName = $ScopeDisplayName + ManagementGroupId = $ManagementGroupId + ManagementGroupDisplayName = $ManagementGroupDisplayName + SubscriptionId = $SubscriptionId + SubscriptionDisplayName = $SubscriptionDisplayName + MgPath = $MgPath + MgLevel = $MgLevel + RoleId = $entry.roleDefinition.externalId + RoleIdGuid = $entry.roleDefinition.externalId -replace '.*/' + RoleType = $roleType + RoleName = $entry.roleDefinition.displayName + IdentityObjectId = $entry.subject.id + IdentityType = $principalType + IdentityDisplayName = $entry.subject.displayName + IdentityPrincipalName = $entry.subject.principalName + PIMId = $entry.id + PIMInheritance = $entry.memberType + PIMInheritedFromClear = '' + PIMInheritedFrom = '' + PIMStartDateTime = $entry.startDateTime + PIMEndDateTime = $entry.endDateTime + }) } } - else { - $principalType = $entry.subject.type - } - - $roleType = 'undefined' - if ($entry.roleDefinition.type -eq 'BuiltInRole') { $roleType = 'Builtin' } - if ($entry.roleDefinition.type -eq 'CustomRole') { $roleType = 'Custom' } - - $null = $script:arrayPIMEligible.Add([PSCustomObject]@{ - ScopeType = $ScopeType - ScopeId = $scopeId - ScopeDisplayName = $ScopeDisplayName - ManagementGroupId = $ManagementGroupId - ManagementGroupDisplayName = $ManagementGroupDisplayName - SubscriptionId = $SubscriptionId - SubscriptionDisplayName = $SubscriptionDisplayName - MgPath = $MgPath - MgLevel = $MgLevel - RoleId = $entry.roleDefinition.externalId - RoleIdGuid = $entry.roleDefinition.externalId -replace '.*/' - RoleType = $roleType - RoleName = $entry.roleDefinition.displayName - IdentityObjectId = $entry.subject.id - IdentityType = $principalType - IdentityDisplayName = $entry.subject.displayName - IdentityPrincipalName = $entry.subject.principalName - PIMId = $entry.id - PIMInheritance = $entry.memberType - PIMInheritedFromClear = '' - PIMInheritedFrom = '' - PIMStartDateTime = $entry.startDateTime - PIMEndDateTime = $entry.endDateTime - }) } } - } - } -ThrottleLimit $ThrottleLimit + } -ThrottleLimit $ThrottleLimit + } foreach ($entry in $arrayPIMEligible) { if ($entry.PIMInheritance -eq 'inherited') { @@ -4084,7 +4188,7 @@ function getResourceDiagnosticsCapability { #thx @Jim Britt (Microsoft) https://github.com/JimGBritt/AzurePolicy/tree/master/AzureMonitor/Scripts Create-AzDiagPolicy.ps1 $responseJSON = '' - $logCategories = @() + $logCategories = [System.Collections.ArrayList]@() $metrics = $false $logs = $false @@ -4144,7 +4248,7 @@ function getResourceDiagnosticsCapability { } if ($response.properties.categoryType -eq 'Logs') { $logs = $true - $logCategories += $response.name + $null = $logCategories.Add($response.name) } } } @@ -4239,17 +4343,17 @@ function handleCloudEnvironment { } function NamingValidation($toCheck) { $checks = @(':', '/', '\', '<', '>', '|', '"') - $array = @() + $array = [System.Collections.ArrayList]@() foreach ($check in $checks) { if ($toCheck -like "*$($check)*") { - $array += $check + $null = $array.Add($check) } } if ($toCheck -match '\*') { - $array += '*' + $null = $array.Add('*') } if ($toCheck -match '\?') { - $array += '?' + $null = $array.Add('?') } return $array } @@ -4341,8 +4445,15 @@ function processAADGroups { Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" - $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { - $aadGroupIdWithRoleAssignment = $_ + $ThrottleLimitThis = $ThrottleLimit * 2 + $batchSize = [math]::ceiling($optimizedTableForAADGroupsQuery.Count / $ThrottleLimitThis) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $optimizedTableForAADGroupsQueryBatch = ($optimizedTableForAADGroupsQuery) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($optimizedTableForAADGroupsQueryBatch.Count) batches" + + $optimizedTableForAADGroupsQueryBatch | ForEach-Object -Parallel { + #$aadGroupIdWithRoleAssignment = $_ #region UsingVARs #fromOtherFunctions $AADGroupMembersLimit = $using:AADGroupMembersLimit @@ -4361,41 +4472,41 @@ function processAADGroups { $function:getGroupmembers = $using:funcGetGroupmembers #endregion UsingVARs - $rndom = Get-Random -Minimum 10 -Maximum 750 - Start-Sleep -Millisecond $rndom + foreach ($aadGroupIdWithRoleAssignment in $_.Group) { - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)/transitiveMembers/`$count" - $method = 'GET' - $aadGroupMembersCount = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupMembersCountTransitive $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)" -listenOn 'Content' -consistencyLevel 'eventual' + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)/transitiveMembers/`$count" + $method = 'GET' + $aadGroupMembersCount = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupMembersCountTransitive $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)" -listenOn 'Content' -consistencyLevel 'eventual' - if ($aadGroupMembersCount -eq 'Request_ResourceNotFound') { - $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ - groupId = $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId - }) - } - else { - if ($aadGroupMembersCount -gt $AADGroupMembersLimit) { - Write-Host " Group exceeding limit ($($AADGroupMembersLimit)); memberCount: $aadGroupMembersCount; Group: $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname) ($($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)); Members will not be resolved adjust the limit using parameter -AADGroupMembersLimit" - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) = @{} - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersAllCount = $aadGroupMembersCount - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersUsersCount = 'n/a' - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersGroupsCount = 'n/a' - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersServicePrincipalsCount = 'n/a' + if ($aadGroupMembersCount -eq 'Request_ResourceNotFound') { + $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ + groupId = $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId + }) } else { - getGroupmembers -aadGroupId $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId -aadGroupDisplayName $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname + if ($aadGroupMembersCount -gt $AADGroupMembersLimit) { + Write-Host " Group exceeding limit ($($AADGroupMembersLimit)); memberCount: $aadGroupMembersCount; Group: $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname) ($($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)); Members will not be resolved adjust the limit using parameter -AADGroupMembersLimit" + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) = @{} + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersAllCount = $aadGroupMembersCount + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersUsersCount = 'n/a' + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersGroupsCount = 'n/a' + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersServicePrincipalsCount = 'n/a' + } + else { + getGroupmembers -aadGroupId $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId -aadGroupDisplayName $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname + } } - } - $null = $script:arrayProgressedAADGroups.Add($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) - $processedAADGroupsCount = $null - $processedAADGroupsCount = ($arrayProgressedAADGroups).Count - if ($processedAADGroupsCount) { - if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" + $null = $script:arrayProgressedAADGroups.Add($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) + $processedAADGroupsCount = $null + $processedAADGroupsCount = ($arrayProgressedAADGroups).Count + if ($processedAADGroupsCount) { + if ($processedAADGroupsCount % $indicator -eq 0) { + Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" + } } } - } -ThrottleLimit ($ThrottleLimit * 2) + } -ThrottleLimit ($ThrottleLimitThis) } else { Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" @@ -5003,7 +5114,15 @@ function processApplications { $startSPApp = Get-Date $currentDateUTC = (Get-Date).ToUniversalTime() $script:arrayApplicationRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) - $servicePrincipalsOfTypeApplication | ForEach-Object -Parallel { + + $ThrottleLimitThis = $ThrottleLimit * 2 + $batchSize = [math]::ceiling($servicePrincipalsOfTypeApplication.Count / $ThrottleLimitThis) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $servicePrincipalsOfTypeApplicationBatch = ($servicePrincipalsOfTypeApplication) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($servicePrincipalsOfTypeApplicationBatch.Count) batches" + + $servicePrincipalsOfTypeApplicationBatch | ForEach-Object -Parallel { #region UsingVARs $currentDateUTC = $using:currentDateUTC @@ -5016,91 +5135,92 @@ function processApplications { $htServicePrincipals = $using:htServicePrincipals #endregion UsingVARs - $sp = $htServicePrincipals.($_) + foreach ($entry in $_.Group) { + $sp = $htServicePrincipals.($entry) - $currentTask = "getApp $($sp.appId)" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" - $method = 'GET' - $getApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + $currentTask = "getApp $($sp.appId)" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" + $method = 'GET' + $getApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - if ($getApplication -eq 'Request_ResourceNotFound') { - $null = $script:arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ - appId = $sp.appId - }) - } - else { - if (($getApplication).Count -eq 0) { - Write-Host "$($sp.appId) no data returned / seems non existent?" + if ($getApplication -eq 'Request_ResourceNotFound') { + $null = $script:arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ + appId = $sp.appId + }) } else { - $script:htAppDetails.($sp.id) = @{} - $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType - $script:htAppDetails.($sp.id).spGraphDetails = $sp - $script:htAppDetails.($sp.id).appGraphDetails = $getApplication - - $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count - if ($appPasswordCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount - $appPasswordCredentialsExpiredCount = 0 - $appPasswordCredentialsGracePeriodExpiryCount = 0 - $appPasswordCredentialsExpiryOKCount = 0 - $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appPasswordCredential in $getApplication.passwordCredentials) { - $passwordExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays - if ($passwordExpiryTotalDays -lt 0) { - $appPasswordCredentialsExpiredCount++ - } - elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appPasswordCredentialsGracePeriodExpiryCount++ - } - else { - if ($passwordExpiryTotalDays -gt 730) { - $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ + if (($getApplication).Count -eq 0) { + Write-Host "$($sp.appId) no data returned / seems non existent?" + } + else { + $script:htAppDetails.($sp.id) = @{} + $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType + $script:htAppDetails.($sp.id).spGraphDetails = $sp + $script:htAppDetails.($sp.id).appGraphDetails = $getApplication + + $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count + if ($appPasswordCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount + $appPasswordCredentialsExpiredCount = 0 + $appPasswordCredentialsGracePeriodExpiryCount = 0 + $appPasswordCredentialsExpiryOKCount = 0 + $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appPasswordCredential in $getApplication.passwordCredentials) { + $passwordExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays + if ($passwordExpiryTotalDays -lt 0) { + $appPasswordCredentialsExpiredCount++ + } + elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appPasswordCredentialsGracePeriodExpiryCount++ } else { - $appPasswordCredentialsExpiryOKCount++ + if ($passwordExpiryTotalDays -gt 730) { + $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appPasswordCredentialsExpiryOKCount++ + } } } - } - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount - $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount - } - - $appKeyCredentialsCount = ($getApplication.keyCredentials).count - if ($appKeyCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount - $appKeyCredentialsExpiredCount = 0 - $appKeyCredentialsGracePeriodExpiryCount = 0 - $appKeyCredentialsExpiryOKCount = 0 - $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appKeyCredential in $getApplication.keyCredentials) { - $keyCredentialExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays - if ($keyCredentialExpiryTotalDays -lt 0) { - $appKeyCredentialsExpiredCount++ - } - elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appKeyCredentialsGracePeriodExpiryCount++ - } - else { - if ($keyCredentialExpiryTotalDays -gt 730) { - $appKeyCredentialsExpiryOKMoreThan2YearsCount++ + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount + $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount + } + + $appKeyCredentialsCount = ($getApplication.keyCredentials).count + if ($appKeyCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount + $appKeyCredentialsExpiredCount = 0 + $appKeyCredentialsGracePeriodExpiryCount = 0 + $appKeyCredentialsExpiryOKCount = 0 + $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appKeyCredential in $getApplication.keyCredentials) { + $keyCredentialExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays + if ($keyCredentialExpiryTotalDays -lt 0) { + $appKeyCredentialsExpiredCount++ + } + elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appKeyCredentialsGracePeriodExpiryCount++ } else { - $appKeyCredentialsExpiryOKCount++ + if ($keyCredentialExpiryTotalDays -gt 730) { + $appKeyCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appKeyCredentialsExpiryOKCount++ + } } } + $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount + $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount } - $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount - $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount } } } - - } -ThrottleLimit ($ThrottleLimit * 2) + } -ThrottleLimit ($ThrottleLimitThis) $endSPApp = Get-Date Write-Host "Processing Service Principals - Applications duration: $((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalSeconds) seconds)" @@ -5123,13 +5243,13 @@ function processDataCollection { $btchCnt = 0 foreach ($btch in $mgBatch) { $btchCnt++ - $listOfMGs = @() + $listOfMGs = [System.Collections.ArrayList]@() foreach ($btchMg in $btch.Group | Sort-Object -Property name) { if ($btchMg.name -eq $btchMg.Properties.displayName) { - $listOfMGs += $btchMg.name + $null = $listOfMGs.Add($btchMg.name) } else { - $listOfMGs += "$($btchMg.name) ($($btchMg.Properties.displayName))" + $null = $listOfMGs.Add("$($btchMg.name) ($($btchMg.Properties.displayName))") } } Write-Host " Batch#$($btchCnt) - $($listOfMGs.Count) Management Groups: $($listOfMGs -join ', ')" @@ -5140,8 +5260,13 @@ function processDataCollection { showMemoryUsage - $batchLevel.Group | ForEach-Object -Parallel { - $mgdetail = $_ + $batchSize = [math]::ceiling($batchLevel.Group.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchLevelGroupBatch = ($batchLevel.Group) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($batchLevelGroupBatch.Count) batches" + + $batchLevelGroupBatch | ForEach-Object -Parallel { #region UsingVARs #Parameters MG&Sub related $CsvDelimiter = $using:CsvDelimiter @@ -5216,103 +5341,115 @@ function processDataCollection { #endregion usingVARS $builtInPolicyDefinitionsCount = $using:builtInPolicyDefinitionsCount - $addRowToTableDone = $false + foreach ($mgdetail in $_.Group) { - $MgDetailThis = $htManagementGroupsMgPath.($mgdetail.Name) - $MgParentId = $MgDetailThis.Parent - $hierarchyLevel = $MgDetailThis.ParentNameChainCount + $addRowToTableDone = $false - if ($MgParentId -eq '__TenantRoot__') { - $MgParentId = 'TenantRoot' - $MgParentName = $MgParentId - } - else { - $MgParentName = $htManagementGroupsMgPath.($MgParentId).DisplayName - } + $MgDetailThis = $htManagementGroupsMgPath.($mgdetail.Name) + $MgParentId = $MgDetailThis.Parent + $hierarchyLevel = $MgDetailThis.ParentNameChainCount - $rndom = Get-Random -Minimum 10 -Maximum 750 - Start-Sleep -Millisecond $rndom - $startMgLoopThis = Get-Date + if ($MgParentId -eq '__TenantRoot__') { + $MgParentId = 'TenantRoot' + $MgParentName = $MgParentId + } + else { + $MgParentName = $htManagementGroupsMgPath.($MgParentId).DisplayName + } - if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { + $rndom = Get-Random -Minimum 10 -Maximum 750 + Start-Sleep -Millisecond $rndom + $startMgLoopThis = Get-Date - #namingValidation - if (-not [string]::IsNullOrEmpty($mgdetail.properties.displayName)) { - $namingValidationResult = NamingValidation -toCheck $mgdetail.properties.displayName - if ($namingValidationResult.Count -gt 0) { - $script:htNamingValidation.ManagementGroup.($mgdetail.Name) = @{} - $script:htNamingValidation.ManagementGroup.($mgdetail.Name).nameInvalidChars = ($namingValidationResult -join '') - $script:htNamingValidation.ManagementGroup.($mgdetail.Name).name = $mgdetail.properties.displayName + if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { + + #namingValidation + if (-not [string]::IsNullOrEmpty($mgdetail.properties.displayName)) { + $namingValidationResult = NamingValidation -toCheck $mgdetail.properties.displayName + if ($namingValidationResult.Count -gt 0) { + $script:htNamingValidation.ManagementGroup.($mgdetail.Name) = @{} + $script:htNamingValidation.ManagementGroup.($mgdetail.Name).nameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.ManagementGroup.($mgdetail.Name).name = $mgdetail.properties.displayName + } } - } - $targetMgOrSub = 'MG' - $baseParameters = @{ - scopeId = $mgdetail.Name - scopeDisplayName = $mgdetail.properties.displayName - } + $targetMgOrSub = 'MG' + $baseParameters = @{ + scopeId = $mgdetail.Name + scopeDisplayName = $mgdetail.properties.displayName + } - #ManagementGroupASCSecureScore - $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $mgdetail.Name + #ManagementGroupASCSecureScore + $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $mgdetail.Name - $addRowToTableParameters = @{ - hierarchyLevel = $hierarchyLevel - mgParentId = $mgParentId - mgParentName = $mgParentName - mgAscSecureScoreResult = $mgAscSecureScoreResult - } + $addRowToTableParameters = @{ + hierarchyLevel = $hierarchyLevel + mgParentId = $mgParentId + mgParentName = $mgParentName + mgAscSecureScoreResult = $mgAscSecureScoreResult + } - #mg diag - DataCollectionDiagnosticsMG @baseParameters + #mg diag + DataCollectionDiagnosticsMG @baseParameters - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - #MGPolicyCompliance - DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub - } + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + #MGPolicyCompliance + DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub + } - #MGBlueprintDefinitions - $functionReturn = DataCollectionBluePrintDefinitionsMG @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } + #MGBlueprintDefinitions + $functionReturn = DataCollectionBluePrintDefinitionsMG @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } - #MGPolicyExemptions - DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub + #MGPolicyExemptions + DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub - #MGPolicyDefinitions - $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' + #MGPolicyDefinitions + $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' - #MGPolicySetDefinitions - $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' + #MGPolicySetDefinitions + $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' - if (-not $htMgAtScopePoliciesScoped.($mgdetail.Name)) { - $script:htMgAtScopePoliciesScoped.($mgdetail.Name) = @{} - $script:htMgAtScopePoliciesScoped.($mgdetail.Name).ScopedCount = $policyDefinitionsScopedCount + $policySetDefinitionsScopedCount - } + if (-not $htMgAtScopePoliciesScoped.($mgdetail.Name)) { + $script:htMgAtScopePoliciesScoped.($mgdetail.Name) = @{} + $script:htMgAtScopePoliciesScoped.($mgdetail.Name).ScopedCount = $policyDefinitionsScopedCount + $policySetDefinitionsScopedCount + } - $scopedPolicyCounts = @{ - policyDefinitionsScopedCount = $policyDefinitionsScopedCount - policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount - } + $scopedPolicyCounts = @{ + policyDefinitionsScopedCount = $policyDefinitionsScopedCount + policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount + } - #MgPolicyAssignments - $functionReturn = DataCollectionPolicyAssignmentsMG @baseParameters @addRowToTableParameters @scopedPolicyCounts - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } + #MgPolicyAssignments + $functionReturn = DataCollectionPolicyAssignmentsMG @baseParameters @addRowToTableParameters @scopedPolicyCounts + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } - #MGRoleDefinitions - DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + #MGRoleDefinitions + DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - #MGRoleAssignments - $functionReturn = DataCollectionRoleAssignmentsMG @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } + #MGRoleAssignments + $functionReturn = DataCollectionRoleAssignmentsMG @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } - if ($addRowToTableDone -ne $true) { + if ($addRowToTableDone -ne $true) { + addRowToTable ` + -level $hierarchyLevel ` + -mgName $mgdetail.properties.displayName ` + -mgId $mgdetail.Name ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult + } + } + else { addRowToTable ` -level $hierarchyLevel ` -mgName $mgdetail.properties.displayName ` @@ -5321,29 +5458,19 @@ function processDataCollection { -mgParentName $mgParentName ` -mgASCSecureScore $mgAscSecureScoreResult } - } - else { - addRowToTable ` - -level $hierarchyLevel ` - -mgName $mgdetail.properties.displayName ` - -mgId $mgdetail.Name ` - -mgParentId $mgParentId ` - -mgParentName $mgParentName ` - -mgASCSecureScore $mgAscSecureScoreResult - } - $endMgLoopThis = Get-Date - $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ - Type = 'Mg' - Id = $mgdetail.Name - DurationSec = (New-TimeSpan -Start $startMgLoopThis -End $endMgLoopThis).TotalSeconds - }) - - $null = $script:arrayDataCollectionProgressMg.Add($mgdetail.Name) - $progressCount = ($arrayDataCollectionProgressMg).Count - Write-Host " $($progressCount)/$($allManagementGroupsFromEntitiesChildOfRequestedMgCount) Management Groups processed" + $endMgLoopThis = Get-Date + $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ + Type = 'Mg' + Id = $mgdetail.Name + DurationSec = (New-TimeSpan -Start $startMgLoopThis -End $endMgLoopThis).TotalSeconds + }) + $null = $script:arrayDataCollectionProgressMg.Add($mgdetail.Name) + $progressCount = ($arrayDataCollectionProgressMg).Count + Write-Host " $($progressCount)/$($allManagementGroupsFromEntitiesChildOfRequestedMgCount) Management Groups processed" + } } -ThrottleLimit $ThrottleLimit } @@ -5378,133 +5505,128 @@ function processDataCollection { $startSubLoop = Get-Date if ($subsToProcessInCustomDataCollectionCount -gt 0) { + $batchSize = [math]::ceiling($subsToProcessInCustomDataCollectionCount / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" $counterBatch = [PSCustomObject] @{ Value = 0 } - $batchSize = 100 - if ($subsToProcessInCustomDataCollectionCount -gt 500) { - $batchSize = 200 - } - Write-Host " Subscriptions Batch size: $batchSize" + $subsToProcessInCustomDataCollectionBatch = ($subsToProcessInCustomDataCollection) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($subsToProcessInCustomDataCollectionBatch.Count) batches" - $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } - $batchCnt = 0 - foreach ($batch in $subscriptionsBatch) { - $startBatch = Get-Date - $batchCnt++ - Write-Host " processing Batch #$batchCnt/$(($subscriptionsBatch | Measure-Object).Count) ($(($batch.Group | Measure-Object).Count) Subscriptions)" - showMemoryUsage + $startBatch = Get-Date + showMemoryUsage - $batch.Group | ForEach-Object -Parallel { - $startSubLoopThis = Get-Date - $childMgSubDetail = $_ - #region UsingVARs - #Parameters MG&Sub related - $CsvDelimiter = $using:CsvDelimiter - $CsvDelimiterOpposite = $using:CsvDelimiterOpposite - #Parameters Sub related - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $newTable = $using:newTable - $storageAccounts = $using:storageAccounts - $resourcesAll = $using:resourcesAll - $resourcesIdsAll = $using:resourcesIdsAll - $resourceGroupsAll = $using:resourceGroupsAll - $customDataCollectionDuration = $using:customDataCollectionDuration - $htSubscriptionsMgPath = $using:htSubscriptionsMgPath - $htManagementGroupsMgPath = $using:htManagementGroupsMgPath - $htResourceProvidersAll = $using:htResourceProvidersAll - $arrayFeaturesAll = $using:arrayFeaturesAll - $htSubscriptionTagList = $using:htSubscriptionTagList - $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource - $htAllTagList = $using:htAllTagList - $htSubscriptionTags = $using:htSubscriptionTags - $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy - $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet - $htCacheDefinitionsRole = $using:htCacheDefinitionsRole - $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint - $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy - $htCachePolicyComplianceSUB = $using:htCachePolicyComplianceSUB - $htCachePolicyComplianceResponseTooLargeSUB = $using:htCachePolicyComplianceResponseTooLargeSUB - $htCacheAssignmentsRole = $using:htCacheAssignmentsRole - $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources - $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint - $htCacheAssignmentsPolicyOnResourceGroupsAndResources = $using:htCacheAssignmentsPolicyOnResourceGroupsAndResources - $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy - $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions - $htResourceLocks = $using:htResourceLocks - $LimitPOLICYPolicyDefinitionsScopedSubscription = $using:LimitPOLICYPolicyDefinitionsScopedSubscription - $LimitPOLICYPolicySetDefinitionsScopedSubscription = $using:LimitPOLICYPolicySetDefinitionsScopedSubscription - $LimitPOLICYPolicyAssignmentsSubscription = $using:LimitPOLICYPolicyAssignmentsSubscription - $LimitPOLICYPolicySetAssignmentsSubscription = $using:LimitPOLICYPolicySetAssignmentsSubscription - $childrenSubscriptionsCount = $using:childrenSubscriptionsCount - $subsToProcessInCustomDataCollectionCount = $using:subsToProcessInCustomDataCollectionCount - $arrayDataCollectionProgressSub = $using:arrayDataCollectionProgressSub - $arraySubResourcesAddArrayDuration = $using:arraySubResourcesAddArrayDuration - $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI - $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI - $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub - $htMgASCSecureScore = $using:htMgASCSecureScore - $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention - $htNamingValidation = $using:htNamingValidation - $htPrincipals = $using:htPrincipals - $htServicePrincipals = $using:htServicePrincipals - $htUserTypesGuest = $using:htUserTypesGuest - $arrayDefenderPlans = $using:arrayDefenderPlans - $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped - $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources - $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit - $arrayPsRule = $using:arrayPsRule - $arrayPSRuleTracking = $using:arrayPSRuleTracking - $htClassicAdministrators = $using:htClassicAdministrators - $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM - $alzPolicies = $using:alzPolicies - $alzPolicySets = $using:alzPolicySets - $alzPolicyHashes = $using:alzPolicyHashes - $alzPolicySetHashes = $using:alzPolicySetHashes - $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances - $htDefenderEmailContacts = $using:htDefenderEmailContacts - $arrayVNets = $using:arrayVNets - $arrayPrivateEndPoints = $using:arrayPrivateEndPoints - $htResourceProvidersRef = $using:htResourceProvidersRef - $arrayPrivateEndPointsFromResourceProperties = $using:arrayPrivateEndPointsFromResourceProperties - $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed - $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes - $arrayAdvisorScores = $using:arrayAdvisorScores - $ValidPolicyEffects = $using:ValidPolicyEffects - #$htResourcesWithProperties = $using:htResourcesWithProperties - #other - $function:addRowToTable = $using:funcAddRowToTable - $function:namingValidation = $using:funcNamingValidation - $function:resolveObjectIds = $using:funcResolveObjectIds - $function:testGuid = $using:funcTestGuid - $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore - $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans - $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub - $function:dataCollectionResources = $using:funcDataCollectionResources - $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts - $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups - $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders - $function:dataCollectionFeatures = $using:funcDataCollectionFeatures - $function:dataCollectionResourceLocks = $using:funcDataCollectionResourceLocks - $function:dataCollectionTags = $using:funcDataCollectionTags - $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates - $function:dataCollectionASCSecureScoreSub = $using:funcDataCollectionASCSecureScoreSub - $function:dataCollectionBluePrintDefinitionsSub = $using:funcDataCollectionBluePrintDefinitionsSub - $function:dataCollectionBluePrintAssignmentsSub = $using:funcDataCollectionBluePrintAssignmentsSub - $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions - $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions - $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions - $function:dataCollectionPolicyAssignmentsSub = $using:funcDataCollectionPolicyAssignmentsSub - $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions - $function:dataCollectionRoleAssignmentsSub = $using:funcDataCollectionRoleAssignmentsSub - $function:dataCollectionClassicAdministratorsSub = $using:funcDataCollectionClassicAdministratorsSub - $function:dataCollectionDefenderEmailContacts = $using:funcDataCollectionDefenderEmailContacts - $function:dataCollectionVNets = $using:funcDataCollectionVNets - $function:dataCollectionPrivateEndpoints = $using:funcDataCollectionPrivateEndpoints - $function:dataCollectionAdvisorScores = $using:funcDataCollectionAdvisorScores - $function:detectPolicyEffect = $using:funcDetectPolicyEffect - #endregion UsingVARs + $subsToProcessInCustomDataCollectionBatch | ForEach-Object -Parallel { + $startSubLoopThis = Get-Date + #region UsingVARs + #Parameters MG&Sub related + $CsvDelimiter = $using:CsvDelimiter + $CsvDelimiterOpposite = $using:CsvDelimiterOpposite + #Parameters Sub related + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $newTable = $using:newTable + $storageAccounts = $using:storageAccounts + $resourcesAll = $using:resourcesAll + $resourcesIdsAll = $using:resourcesIdsAll + $resourceGroupsAll = $using:resourceGroupsAll + $customDataCollectionDuration = $using:customDataCollectionDuration + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htManagementGroupsMgPath = $using:htManagementGroupsMgPath + $htResourceProvidersAll = $using:htResourceProvidersAll + $arrayFeaturesAll = $using:arrayFeaturesAll + $htSubscriptionTagList = $using:htSubscriptionTagList + $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource + $htAllTagList = $using:htAllTagList + $htSubscriptionTags = $using:htSubscriptionTags + $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy + $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet + $htCacheDefinitionsRole = $using:htCacheDefinitionsRole + $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint + $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy + $htCachePolicyComplianceSUB = $using:htCachePolicyComplianceSUB + $htCachePolicyComplianceResponseTooLargeSUB = $using:htCachePolicyComplianceResponseTooLargeSUB + $htCacheAssignmentsRole = $using:htCacheAssignmentsRole + $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources + $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint + $htCacheAssignmentsPolicyOnResourceGroupsAndResources = $using:htCacheAssignmentsPolicyOnResourceGroupsAndResources + $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy + $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions + $htResourceLocks = $using:htResourceLocks + $LimitPOLICYPolicyDefinitionsScopedSubscription = $using:LimitPOLICYPolicyDefinitionsScopedSubscription + $LimitPOLICYPolicySetDefinitionsScopedSubscription = $using:LimitPOLICYPolicySetDefinitionsScopedSubscription + $LimitPOLICYPolicyAssignmentsSubscription = $using:LimitPOLICYPolicyAssignmentsSubscription + $LimitPOLICYPolicySetAssignmentsSubscription = $using:LimitPOLICYPolicySetAssignmentsSubscription + $childrenSubscriptionsCount = $using:childrenSubscriptionsCount + $subsToProcessInCustomDataCollectionCount = $using:subsToProcessInCustomDataCollectionCount + $arrayDataCollectionProgressSub = $using:arrayDataCollectionProgressSub + $arraySubResourcesAddArrayDuration = $using:arraySubResourcesAddArrayDuration + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI + $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub + $htMgASCSecureScore = $using:htMgASCSecureScore + $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention + $htNamingValidation = $using:htNamingValidation + $htPrincipals = $using:htPrincipals + $htServicePrincipals = $using:htServicePrincipals + $htUserTypesGuest = $using:htUserTypesGuest + $arrayDefenderPlans = $using:arrayDefenderPlans + $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped + $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources + $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit + $arrayPsRule = $using:arrayPsRule + $arrayPSRuleTracking = $using:arrayPSRuleTracking + $htClassicAdministrators = $using:htClassicAdministrators + $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM + $alzPolicies = $using:alzPolicies + $alzPolicySets = $using:alzPolicySets + $alzPolicyHashes = $using:alzPolicyHashes + $alzPolicySetHashes = $using:alzPolicySetHashes + $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances + $htDefenderEmailContacts = $using:htDefenderEmailContacts + $arrayVNets = $using:arrayVNets + $arrayPrivateEndPoints = $using:arrayPrivateEndPoints + $htResourceProvidersRef = $using:htResourceProvidersRef + $arrayPrivateEndPointsFromResourceProperties = $using:arrayPrivateEndPointsFromResourceProperties + $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed + $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes + $arrayAdvisorScores = $using:arrayAdvisorScores + $ValidPolicyEffects = $using:ValidPolicyEffects + #$htResourcesWithProperties = $using:htResourcesWithProperties + #other + $function:addRowToTable = $using:funcAddRowToTable + $function:namingValidation = $using:funcNamingValidation + $function:resolveObjectIds = $using:funcResolveObjectIds + $function:testGuid = $using:funcTestGuid + $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore + $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans + $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub + $function:dataCollectionResources = $using:funcDataCollectionResources + $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts + $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups + $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders + $function:dataCollectionFeatures = $using:funcDataCollectionFeatures + $function:dataCollectionResourceLocks = $using:funcDataCollectionResourceLocks + $function:dataCollectionTags = $using:funcDataCollectionTags + $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates + $function:dataCollectionASCSecureScoreSub = $using:funcDataCollectionASCSecureScoreSub + $function:dataCollectionBluePrintDefinitionsSub = $using:funcDataCollectionBluePrintDefinitionsSub + $function:dataCollectionBluePrintAssignmentsSub = $using:funcDataCollectionBluePrintAssignmentsSub + $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions + $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions + $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions + $function:dataCollectionPolicyAssignmentsSub = $using:funcDataCollectionPolicyAssignmentsSub + $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions + $function:dataCollectionRoleAssignmentsSub = $using:funcDataCollectionRoleAssignmentsSub + $function:dataCollectionClassicAdministratorsSub = $using:funcDataCollectionClassicAdministratorsSub + $function:dataCollectionDefenderEmailContacts = $using:funcDataCollectionDefenderEmailContacts + $function:dataCollectionVNets = $using:funcDataCollectionVNets + $function:dataCollectionPrivateEndpoints = $using:funcDataCollectionPrivateEndpoints + $function:dataCollectionAdvisorScores = $using:funcDataCollectionAdvisorScores + $function:detectPolicyEffect = $using:funcDetectPolicyEffect + #endregion UsingVARs + + foreach ($childMgSubDetail in $_.Group) { $addRowToTableDone = $false @@ -5718,12 +5840,11 @@ function processDataCollection { $null = $script:arrayDataCollectionProgressSub.Add($childMgSubId) $progressCount = ($arrayDataCollectionProgressSub).Count Write-Host " $($progressCount)/$($subsToProcessInCustomDataCollectionCount) Subscriptions processed" + } + } -ThrottleLimit $ThrottleLimit - } -ThrottleLimit $ThrottleLimit - - $endBatch = Get-Date - Write-Host " Batch #$batchCnt processing duration: $((New-TimeSpan -Start $startBatch -End $endBatch).TotalMinutes) minutes ($((New-TimeSpan -Start $startBatch -End $endBatch).TotalSeconds) seconds)" - } + $endBatch = Get-Date + Write-Host " Batch #$batchCnt processing duration: $((New-TimeSpan -Start $startBatch -End $endBatch).TotalMinutes) minutes ($((New-TimeSpan -Start $startBatch -End $endBatch).TotalSeconds) seconds)" $endSubLoop = Get-Date Write-Host " CustomDataCollection Subscriptions processing duration: $((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalSeconds) seconds)" @@ -5733,7 +5854,6 @@ function processDataCollection { Write-Host " CustomDataCollection Subscriptions 'PSRule for Azure' processing duration (in sum): $($durationPSRuleTotalSeconds / 60) minutes ($($durationPSRuleTotalSeconds) seconds)" } } - #test Write-Host " built-in PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'BuiltIn'}).Count)" Write-Host " custom PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'Custom'}).Count)" Write-Host " all PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.Count)" @@ -6512,9 +6632,8 @@ function processDefinitionInsights() { ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments) } else { - $array = @() - $array += ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments - $array += $htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments + $array = [System.Collections.ArrayList]@() + $null = $array.Add($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments) ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = $array } } @@ -6527,9 +6646,8 @@ function processDefinitionInsights() { ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments) } else { - $array = @() - $array += ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments - $array += $htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments + $array = [System.Collections.ArrayList]@() + $null = $array.Add($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments) ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = $array } } @@ -7298,7 +7416,7 @@ tf.init();}} $null = $arrayRoleDefinitionsForCSVExport.Add([PSCustomObject]@{ Name = $role.Name Id = $role.Id - Description = $role.Json.description + Description = $role.Json.properties.description Type = $roleType AssignmentsCount = $assignmentsCount AssignableScopesCount = $AssignableScopesCount @@ -7794,7 +7912,7 @@ function processNetwork { } } else { - $arrayRemoteMGPath = @() + $arrayRemoteMGPath = [System.Collections.ArrayList]@() foreach ($remoteId in $remoteTenantId) { if ($remoteId -eq 'SubscriptionNotFound Tenant unknown') { $remoteMGPath = 'unknown' @@ -7804,10 +7922,10 @@ function processNetwork { $objectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" + $null = $arrayRemoteMGPath.Add("$remoteId (MS)") } else { - $arrayRemoteMGPath += $remoteId + $null = $arrayRemoteMGPath.Add($remoteId) } if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { $peeringXTenant = 'false' @@ -8218,15 +8336,15 @@ function processPrivateEndpoints { else { $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($peSubscriptionId)?api-version=2020-01-01" $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($peSubscriptionId)'" - $arrayRemoteMGPath = @() + $arrayRemoteMGPath = [System.Collections.ArrayList]@() foreach ($remoteId in $remoteTenantId) { $objectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" + $null = $arrayRemoteMGPath.Add("$remoteId (MS)") } else { - $arrayRemoteMGPath += $remoteId + $null = $arrayRemoteMGPath.Add($remoteId) } if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { $peXTenant = $false @@ -8377,15 +8495,15 @@ function processPrivateEndpoints { else { $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($resourceSubscriptionId)?api-version=2020-01-01" $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($resourceSubscriptionId)'" - $arrayRemoteMGPath = @() + $arrayRemoteMGPath = [System.Collections.ArrayList]@() foreach ($remoteId in $remoteTenantId) { $objectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" + $null = $arrayRemoteMGPath.Add("$remoteId (MS)") } else { - $arrayRemoteMGPath += $remoteId + $null = $arrayRemoteMGPath.Add($remoteId) } if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { $resourceXTenant = $false @@ -8605,7 +8723,7 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc
    Subscription Path: $subPath
    State: $subscriptionState
    QuotaId: $subscriptionQuotaId
    Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
    Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , learn
    Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState
    Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity
    Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles
    @@ -9096,7 +9214,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs + Tag Name Usage ($tagsUsageCount Tags) learn "@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9380,7 +9498,7 @@ extensions: [{ name: 'sort' }]
    -   Set up preview features in Azure subscription docs +   Set up preview features in Azure subscription learn
    @@ -9446,7 +9564,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs + 0 enabled Subscription Features learn '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9473,7 +9591,7 @@ extensions: [{ name: 'sort' }]
    -   Considerations before applying locks docs +   Considerations before applying locks learn
    @@ -9541,7 +9659,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs + 0 Resource Locks learn '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -9559,7 +9677,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@" - +
    $(($mgAllChildMgs).count -1) ManagementGroups below this scope
    $(($mgAllChildSubscriptions).count) Subscriptions below this scope
    Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
    Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , learn
    "@) @@ -9695,7 +9813,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs + No Management Group Diagnostic settings learn '@) } #endregion ScopeInsightsDiagnosticsMg @@ -10070,7 +10188,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@"
    -   CAF - Recommended abbreviations for Azure resource types docs
    +   CAF - Recommended abbreviations for Azure resource types learn
       Resource details can be found in the CSV output *_ResourcesAll.csv
       Download CSV semicolon | comma @@ -10621,7 +10739,7 @@ extensions: [{ name: 'sort' }]
    -   Managed identity 'user-assigned' vs 'system-assigned' docs
    +   Managed identity 'user-assigned' vs 'system-assigned' learn
       Download CSV semicolon | comma
    @@ -12424,8 +12542,13 @@ function processStorageAccountAnalysis { } } - $storageAccounts | ForEach-Object -Parallel { - $storageAccount = $_ + $batchSize = [math]::ceiling($storageAccounts.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $storageAccountsBatch = ($storageAccounts) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($storageAccountsBatch.Count) batches" + + $storageAccountsBatch | ForEach-Object -Parallel { $azAPICallConf = $using:azAPICallConf $arrayStorageAccountAnalysisResults = $using:arrayStorageAccountAnalysisResults $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI @@ -12435,317 +12558,320 @@ function processStorageAccountAnalysis { $htSACost = $using:htSACost $StorageAccountAccessAnalysisSubscriptionTags = $using:StorageAccountAccessAnalysisSubscriptionTags $StorageAccountAccessAnalysisStorageAccountTags = $using:StorageAccountAccessAnalysisStorageAccountTags - $listContainersSuccess = 'n/a' - $containersCount = 'n/a' - $arrayContainers = @() - $arrayContainersAnonymousContainer = @() - $arrayContainersAnonymousBlob = @() - $staticWebsitesState = 'n/a' - $webSiteResponds = 'n/a' - $subscriptionId = ($storageAccount.SA.id -split '/')[2] - $resourceGroupName = ($storageAccount.SA.id -split '/')[4] - $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails - Write-Host "Processing Storage Account '$($storageAccount.SA.name)' - Subscription: '$($subDetails.displayName)' ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)]" + foreach ($storageAccount in $_.Group) { + $listContainersSuccess = 'n/a' + $containersCount = 'n/a' + $arrayContainers = [System.Collections.ArrayList]@() + $arrayContainersAnonymousContainer = [System.Collections.ArrayList]@() + $arrayContainersAnonymousBlob = [System.Collections.ArrayList]@() + $staticWebsitesState = 'n/a' + $webSiteResponds = 'n/a' - if ($storageAccount.SA.Properties.primaryEndpoints.blob) { + $subscriptionId = ($storageAccount.SA.id -split '/')[2] + $resourceGroupName = ($storageAccount.SA.id -split '/')[4] + $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails - $urlServiceProps = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?restype=service&comp=properties" - $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get restype=service&comp=properties" -saResourceGroupName $resourceGroupName -unhandledErrorAction Continue - if ($saProperties) { - if ($saProperties -eq 'AuthorizationFailure' -or $saProperties -eq 'AuthorizationPermissionDenied' -or $saProperties -eq 'ResourceUnavailable' -or $saProperties -eq 'AuthorizationPermissionMismatch' ) { - if ($saProperties -eq 'ResourceUnavailable') { - $staticWebsitesState = $saProperties - } - } - else { - try { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if ($saProperties.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$saProperties - $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) - } + Write-Host "Processing Storage Account '$($storageAccount.SA.name)' - Subscription: '$($subDetails.displayName)' ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)]" + + if ($storageAccount.SA.Properties.primaryEndpoints.blob) { - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions - if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { - if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { - $staticWebsitesState = $true + $urlServiceProps = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?restype=service&comp=properties" + $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get restype=service&comp=properties" -saResourceGroupName $resourceGroupName -unhandledErrorAction Continue + if ($saProperties) { + if ($saProperties -eq 'AuthorizationFailure' -or $saProperties -eq 'AuthorizationPermissionDenied' -or $saProperties -eq 'ResourceUnavailable' -or $saProperties -eq 'AuthorizationPermissionMismatch' ) { + if ($saProperties -eq 'ResourceUnavailable') { + $staticWebsitesState = $saProperties + } + } + else { + try { + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($saProperties.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$saProperties + $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) } - else { - $staticWebsitesState = $false + + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { + $staticWebsitesState = $true + } + else { + $staticWebsitesState = $false + } } } - } - catch { - Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.SA.name)" - Write-Host $($saProperties.ForEach({ [char]$_ }) -join '') -ForegroundColor Cyan + catch { + Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.SA.name)" + Write-Host $($saProperties.ForEach({ [char]$_ }) -join '') -ForegroundColor Cyan + } } } - } - $urlCompList = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?comp=list" - $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get comp=list" -unhandledErrorAction Continue - if ($listContainers) { - if ($listContainers -eq 'AuthorizationFailure' -or $listContainers -eq 'AuthorizationPermissionDenied' -or $listContainers -eq 'ResourceUnavailable' -or $listContainers -eq 'AuthorizationPermissionMismatch') { - if ($listContainers -eq 'ResourceUnavailable') { - $listContainersSuccess = $listContainers + $urlCompList = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?comp=list" + $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get comp=list" -unhandledErrorAction Continue + if ($listContainers) { + if ($listContainers -eq 'AuthorizationFailure' -or $listContainers -eq 'AuthorizationPermissionDenied' -or $listContainers -eq 'ResourceUnavailable' -or $listContainers -eq 'AuthorizationPermissionMismatch') { + if ($listContainers -eq 'ResourceUnavailable') { + $listContainersSuccess = $listContainers + } + else { + $listContainersSuccess = $false + } } else { - $listContainersSuccess = $false - } - } - else { - $listContainersSuccess = $true - } - - if ($listContainersSuccess -eq $true) { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if ($listContainers.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$listContainers - $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) + $listContainersSuccess = $true } - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions - $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count + if ($listContainersSuccess -eq $true) { + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($listContainers.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$listContainers + $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) + } - foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { - $arrayContainers += $container.Name - if ($container.Name -eq '$web' -and $staticWebsitesState) { - if ($storageAccount.SA.properties.primaryEndpoints.web) { - try { - $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.SA.properties.primaryEndpoints.web -Method 'HEAD' - $webSiteResponds = $true - } - catch { - $webSiteResponds = $false + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions + $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count + + foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { + $null = $arrayContainers.Add($container.Name) + if ($container.Name -eq '$web' -and $staticWebsitesState) { + if ($storageAccount.SA.properties.primaryEndpoints.web) { + try { + $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.SA.properties.primaryEndpoints.web -Method 'HEAD' + $webSiteResponds = $true + } + catch { + $webSiteResponds = $false + } } } - } - if ($container.Properties.PublicAccess) { - if ($container.Properties.PublicAccess -eq 'blob') { - $arrayContainersAnonymousBlob += $container.Name - } - if ($container.Properties.PublicAccess -eq 'container') { - $arrayContainersAnonymousContainer += $container.Name + if ($container.Properties.PublicAccess) { + if ($container.Properties.PublicAccess -eq 'blob') { + $null = $arrayContainersAnonymousBlob.Add($container.Name) + } + if ($container.Properties.PublicAccess -eq 'container') { + $null = $arrayContainersAnonymousContainer.Add($container.Name) + } } } } } } - } - $allowSharedKeyAccess = $storageAccount.SA.properties.allowSharedKeyAccess - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowSharedKeyAccess)) { - $allowSharedKeyAccess = 'likely True' - } - $requireInfrastructureEncryption = $storageAccount.SA.properties.encryption.requireInfrastructureEncryption - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.encryption.requireInfrastructureEncryption)) { - $requireInfrastructureEncryption = 'likely False' - } + $allowSharedKeyAccess = $storageAccount.SA.properties.allowSharedKeyAccess + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowSharedKeyAccess)) { + $allowSharedKeyAccess = 'likely True' + } + $requireInfrastructureEncryption = $storageAccount.SA.properties.encryption.requireInfrastructureEncryption + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.encryption.requireInfrastructureEncryption)) { + $requireInfrastructureEncryption = 'likely False' + } - $arrayResourceAccessRules = [System.Collections.ArrayList]@() - if ($storageAccount.SA.properties.networkAcls.resourceAccessRules) { - if ($storageAccount.SA.properties.networkAcls.resourceAccessRules.count -gt 0) { - foreach ($resourceAccessRule in $storageAccount.SA.properties.networkAcls.resourceAccessRules) { + $arrayResourceAccessRules = [System.Collections.ArrayList]@() + if ($storageAccount.SA.properties.networkAcls.resourceAccessRules) { + if ($storageAccount.SA.properties.networkAcls.resourceAccessRules.count -gt 0) { + foreach ($resourceAccessRule in $storageAccount.SA.properties.networkAcls.resourceAccessRules) { - $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' - $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" + $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' + $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" - [regex]$regex = '\*+' - #$resourceAccessRule.resourceId - switch ($regex.matches($resourceAccessRule.resourceId).count) { - { $_ -eq 1 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'resourceGroup' - sort = 3 - }) - } - { $_ -eq 2 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'subscription' - sort = 2 - }) - } - { $_ -eq 3 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'tenant' - sort = 1 - }) - } - default { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'resource' - resource = $resourceAccessRule.resourceId - sort = 0 - }) + [regex]$regex = '\*+' + #$resourceAccessRule.resourceId + switch ($regex.matches($resourceAccessRule.resourceId).count) { + { $_ -eq 1 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resourceGroup' + sort = 3 + }) + } + { $_ -eq 2 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'subscription' + sort = 2 + }) + } + { $_ -eq 3 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'tenant' + sort = 1 + }) + } + default { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resource' + resource = $resourceAccessRule.resourceId + sort = 0 + }) + } } } } } - } - $resourceAccessRulesCount = $arrayResourceAccessRules.count - if ($resourceAccessRulesCount -eq 0) { - $resourceAccessRules = '' - } - else { - $ht = @{} - foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { + $resourceAccessRulesCount = $arrayResourceAccessRules.count + if ($resourceAccessRulesCount -eq 0) { + $resourceAccessRules = '' + } + else { + $ht = @{} + foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { - if ($accessRulePerRange.Name -eq 'resource') { - $arrayResources = @() - foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { - $arrayResources += $resource + if ($accessRulePerRange.Name -eq 'resource') { + $arrayResources = [System.Collections.ArrayList]@() + foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { + $null = $arrayResources.Add($resource) + } + $ht.($accessRulePerRange.Name) = ($arrayResources) } - $ht.($accessRulePerRange.Name) = [array]($arrayResources) - } - else { - $arrayResourceTypes = @() - foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { - $arrayResourceTypes += $resourceType + else { + $arrayResourceTypes = [System.Collections.ArrayList]@() + foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { + $null = $arrayResourceTypes.Add($resourceType) + } + $ht.($accessRulePerRange.Name) = ($arrayResourceTypes) } - $ht.($accessRulePerRange.Name) = [array]($arrayResourceTypes) } + $resourceAccessRules = $ht | ConvertTo-Json } - $resourceAccessRules = $ht | ConvertTo-Json - } - - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.publicNetworkAccess)) { - $publicNetworkAccess = 'likely Enabled' - } - else { - $publicNetworkAccess = $storageAccount.SA.properties.publicNetworkAccess - } - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowedCopyScope)) { - $allowedCopyScope = 'From any Storage Account' - } - else { - $allowedCopyScope = $storageAccount.SA.properties.allowedCopyScope - } + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.publicNetworkAccess)) { + $publicNetworkAccess = 'likely Enabled' + } + else { + $publicNetworkAccess = $storageAccount.SA.properties.publicNetworkAccess + } - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowCrossTenantReplication)) { - if ($allowedCopyScope -ne 'From any Storage Account') { - $allowCrossTenantReplication = "likely False (allowedCopyScope=$allowedCopyScope)" + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowedCopyScope)) { + $allowedCopyScope = 'From any Storage Account' } else { - $allowCrossTenantReplication = 'likely True' + $allowedCopyScope = $storageAccount.SA.properties.allowedCopyScope } - } - else { - $allowCrossTenantReplication = $storageAccount.SA.properties.allowCrossTenantReplication - } - if ($storageAccount.SA.properties.dnsEndpointType) { - $dnsEndpointType = $storageAccount.SA.properties.dnsEndpointType - } - else { - $dnsEndpointType = 'standard' - } + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowCrossTenantReplication)) { + if ($allowedCopyScope -ne 'From any Storage Account') { + $allowCrossTenantReplication = "likely False (allowedCopyScope=$allowedCopyScope)" + } + else { + $allowCrossTenantReplication = 'likely True' + } + } + else { + $allowCrossTenantReplication = $storageAccount.SA.properties.allowCrossTenantReplication + } - if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { - if ($htSACost.($storageAccount.SA.id)) { - $hlpCost = $htSACost.($storageAccount.SA.id) - $saCost = $hlpCost.costAll - $saCostCurrency = $hlpCost.currencyAll - $saCostMeterCategories = $hlpCost.meterCategoryAll + if ($storageAccount.SA.properties.dnsEndpointType) { + $dnsEndpointType = $storageAccount.SA.properties.dnsEndpointType } else { - $saCost = 'n/a' - $saCostCurrency = 'n/a' - $saCostMeterCategories = 'n/a' + $dnsEndpointType = 'standard' } - } - else { - $saCost = '' - $saCostCurrency = '' - $saCostMeterCategories = '' - } - - $temp = [System.Collections.ArrayList]@() - $null = $temp.Add([PSCustomObject]@{ - storageAccount = $storageAccount.SA.name - kind = $storageAccount.SA.kind - skuName = $storageAccount.SA.sku.name - skuTier = $storageAccount.SA.sku.tier - location = $storageAccount.SA.location - creationTime = $storageAccount.SA.properties.creationTime - allowBlobPublicAccess = $storageAccount.SA.properties.allowBlobPublicAccess - publicNetworkAccess = $publicNetworkAccess - SubscriptionId = $subscriptionId - SubscriptionName = $subDetails.displayName - subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId - subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' - resourceGroup = $resourceGroupName - networkAclsdefaultAction = $storageAccount.SA.properties.networkAcls.defaultAction - staticWebsitesState = $staticWebsitesState - staticWebsitesResponse = $webSiteResponds - containersCanBeListed = $listContainersSuccess - containersCount = $containersCount - containers = $arrayContainers -join "$CSVDelimiterOpposite " - containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count - containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " - containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count - containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " - ipRulesCount = $storageAccount.SA.properties.networkAcls.ipRules.Count - ipRulesIPAddressList = ($storageAccount.SA.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " - virtualNetworkRulesCount = $storageAccount.SA.properties.networkAcls.virtualNetworkRules.Count - virtualNetworkRulesList = ($storageAccount.SA.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " - resourceAccessRulesCount = $resourceAccessRulesCount - resourceAccessRules = $resourceAccessRules - bypass = ($storageAccount.SA.properties.networkAcls.bypass | Sort-Object) -join "$CSVDelimiterOpposite " - supportsHttpsTrafficOnly = $storageAccount.SA.properties.supportsHttpsTrafficOnly - minimumTlsVersion = $storageAccount.SA.properties.minimumTlsVersion - allowSharedKeyAccess = $allowSharedKeyAccess - requireInfrastructureEncryption = $requireInfrastructureEncryption - allowedCopyScope = $allowedCopyScope - allowCrossTenantReplication = $allowCrossTenantReplication - dnsEndpointType = $dnsEndpointType - usedCapacity = $storageAccount.SAUsedCapacity - cost = $saCost - metercategory = $saCostMeterCategories - curreny = $saCostCurrency - }) - if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { - foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { - if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { - $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + if ($htSACost.($storageAccount.SA.id)) { + $hlpCost = $htSACost.($storageAccount.SA.id) + $saCost = $hlpCost.costAll + $saCostCurrency = $hlpCost.currencyAll + $saCostMeterCategories = $hlpCost.meterCategoryAll } else { - $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + $saCost = 'n/a' + $saCostCurrency = 'n/a' + $saCostMeterCategories = 'n/a' } } - } + else { + $saCost = '' + $saCostCurrency = '' + $saCostMeterCategories = '' + } + + $temp = [System.Collections.ArrayList]@() + $null = $temp.Add([PSCustomObject]@{ + storageAccount = $storageAccount.SA.name + kind = $storageAccount.SA.kind + skuName = $storageAccount.SA.sku.name + skuTier = $storageAccount.SA.sku.tier + location = $storageAccount.SA.location + creationTime = $storageAccount.SA.properties.creationTime + allowBlobPublicAccess = $storageAccount.SA.properties.allowBlobPublicAccess + publicNetworkAccess = $publicNetworkAccess + SubscriptionId = $subscriptionId + SubscriptionName = $subDetails.displayName + subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId + subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' + resourceGroup = $resourceGroupName + networkAclsdefaultAction = $storageAccount.SA.properties.networkAcls.defaultAction + staticWebsitesState = $staticWebsitesState + staticWebsitesResponse = $webSiteResponds + containersCanBeListed = $listContainersSuccess + containersCount = $containersCount + containers = $arrayContainers -join "$CSVDelimiterOpposite " + containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count + containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " + containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count + containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " + ipRulesCount = $storageAccount.SA.properties.networkAcls.ipRules.Count + ipRulesIPAddressList = ($storageAccount.SA.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " + virtualNetworkRulesCount = $storageAccount.SA.properties.networkAcls.virtualNetworkRules.Count + virtualNetworkRulesList = ($storageAccount.SA.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " + resourceAccessRulesCount = $resourceAccessRulesCount + resourceAccessRules = $resourceAccessRules + bypass = ($storageAccount.SA.properties.networkAcls.bypass | Sort-Object) -join "$CSVDelimiterOpposite " + supportsHttpsTrafficOnly = $storageAccount.SA.properties.supportsHttpsTrafficOnly + minimumTlsVersion = $storageAccount.SA.properties.minimumTlsVersion + allowSharedKeyAccess = $allowSharedKeyAccess + requireInfrastructureEncryption = $requireInfrastructureEncryption + allowedCopyScope = $allowedCopyScope + allowCrossTenantReplication = $allowCrossTenantReplication + dnsEndpointType = $dnsEndpointType + usedCapacity = $storageAccount.SAUsedCapacity + cost = $saCost + metercategory = $saCostMeterCategories + curreny = $saCostCurrency + }) - if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { - if ($storageAccount.SA.tags) { - $htAllSATags = @{} - foreach ($saTagName in ($storageAccount.SA.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { - $htAllSATags.$saTagName = $storageAccount.SA.tags.$saTagName + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { + if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } } } - foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { - if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { - $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) + + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + if ($storageAccount.SA.tags) { + $htAllSATags = @{} + foreach ($saTagName in ($storageAccount.SA.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { + $htAllSATags.$saTagName = $storageAccount.SA.tags.$saTagName + } } - else { - $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { + if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } } } - } - - $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) + $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) + } } -ThrottleLimit $ThrottleLimit } else { @@ -15886,8 +16012,8 @@ extensions: [{ name: 'sort' }] $policyType = 'unknown' $policy = 'unknown' - $arrayExemptedPolicies = @() - $arrayExemptedPoliciesCSV = @() + $arrayExemptedPolicies = [System.Collections.ArrayList]@() + $arrayExemptedPoliciesCSV = [System.Collections.ArrayList]@() $policiesExempted = $null $policiesExemptedCSV = $null $policiesExemptedCSVCount = $null @@ -15939,8 +16065,8 @@ extensions: [{ name: 'sort' }] } } - $arrayExemptedPolicies += $policyExempted - $arrayExemptedPoliciesCSV += $policyExemptedCSV + $null = $arrayExemptedPolicies.Add($policyExempted) + $null = $arrayExemptedPoliciesCSV.Add($policyExemptedCSV) } $policiesExempted = "$($arrayExemptedPolicies.Count)/$($policiesTotalCount) (
    $(($arrayExemptedPolicies | Sort-Object) -join '
    '))" @@ -16198,7 +16324,9 @@ extensions: [{ name: 'sort' }] #this if (-not $htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower())) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{} + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{ + roleassignments = [System.Collections.ArrayList]@() + } } if (($htCacheDefinitionsRole).($roleAssignment.RoleDefinitionId).IsCustom) { @@ -16217,12 +16345,15 @@ extensions: [{ name: 'sort' }] }) #this - if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments += $array - } - else { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array - } + # if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { + # $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + # else { + # #$script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array + # $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = [System.Collections.ArrayList]@() + # $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) } } @@ -16235,7 +16366,9 @@ extensions: [{ name: 'sort' }] #this if (-not $htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower())) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{} + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{ + roleassignments = [System.Collections.ArrayList]@() + } } if (($htCacheDefinitionsRole).($roleAssignment.RoleDefinitionId).IsCustom) { @@ -16254,12 +16387,15 @@ extensions: [{ name: 'sort' }] }) #this - if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments += $array - } - else { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array - } + # if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { + # $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + # else { + # #$script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array + # $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = [System.Collections.ArrayList]@() + # $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) } } } @@ -16269,61 +16405,81 @@ extensions: [{ name: 'sort' }] #endregion PolicyAssignmentsRoleAssignmentMapping #region PolicyAssignmentsUniqueRelations - $startPolicyAssignmnetsUniqueRelations = Get-Date - Write-Host ' processing PolicyAssignmnetsUniqueRelations' + $startPolicyAssignmentsUniqueRelations = Get-Date + Write-Host ' processing PolicyAssignmentsUniqueRelations' $htPolicyAssignmentRelatedRoleAssignments = @{} $htPolicyAssignmentRelatedExemptions = @{} foreach ($policyAssignmentIdUnique in $policyBaseQueryUniqueAssignments) { #region relatedRoleAssignments - $relatedRoleAssignmentsArray = @() - $relatedRoleAssignmentsArrayClear = @() + $relatedRoleAssignmentsArray = [System.Collections.ArrayList]@() + $relatedRoleAssignmentsArrayClear = [System.Collections.ArrayList]@() if ($htPolicyAssignmentRoleAssignmentMappingCount -gt 0) { - if ($htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId)) { - foreach ($entry in $htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId).roleassignments) { + $policyAssignmentMapping = $htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId) + if ($null -ne $policyAssignmentMapping) { + foreach ($entry in $policyAssignmentMapping.roleassignments) { if ($entry.roleDefinitionType -eq 'builtin') { - $relatedRoleAssignmentsArray += "$($entry.roleDefinitionName) ($($entry.roleAssignmentId))" + $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") } else { - $relatedRoleAssignmentsArray += "$($entry.roleDefinitionName -replace '<', '<' -replace '>', '>') ($($entry.roleAssignmentId))" + $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName -replace '<', '<' -replace '>', '>') ($($entry.roleAssignmentId))") } - $relatedRoleAssignmentsArrayClear += "$($entry.roleDefinitionName) ($($entry.roleAssignmentId))" + $null = $relatedRoleAssignmentsArrayClear.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") } } } + # if ($htPolicyAssignmentRoleAssignmentMappingCount -gt 0) { + # if ($htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId)) { + # foreach ($entry in $htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId).roleassignments) { + # if ($entry.roleDefinitionType -eq 'builtin') { + # $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") + # } + # else { + # $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName -replace '<', '<' -replace '>', '>') ($($entry.roleAssignmentId))") + # } + # $null = $relatedRoleAssignmentsArrayClear.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") + # } + # } + # } + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId) = @{} if (($relatedRoleAssignmentsArray).count -gt 0) { - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = ($relatedRoleAssignmentsArray | Sort-Object) -join "$CsvDelimiterOpposite " - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = ($relatedRoleAssignmentsArrayClear | Sort-Object) -join "$CsvDelimiterOpposite " + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = ($relatedRoleAssignmentsArray | Sort-Object) -join "$CsvDelimiterOpposite " + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = ($relatedRoleAssignmentsArrayClear | Sort-Object) -join "$CsvDelimiterOpposite " + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId) = @{ + relatedRoleAssignments = ($relatedRoleAssignmentsArray | Sort-Object) -join "$CsvDelimiterOpposite " + relatedRoleAssignmentsClear = ($relatedRoleAssignmentsArrayClear | Sort-Object) -join "$CsvDelimiterOpposite " + } } else { - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = 'none' - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = 'none' + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = 'none' + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = 'none' + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId) = @{ + relatedRoleAssignments = 'none' + relatedRoleAssignmentsClear = 'none' + } } #endregion relatedRoleAssignments #region exemptions - $arrayExemptions = @() + $arrayExemptions = [System.Collections.ArrayList]@() foreach ($exemptionId in $htPolicyAssignmentExemptions.keys) { if ($htPolicyAssignmentExemptions.($exemptionId).exemption.properties.policyAssignmentId -eq $policyAssignmentIdUnique.PolicyAssignmentId) { - $arrayExemptions += $htPolicyAssignmentExemptions.($exemptionId).exemption - if (-not $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId)) { - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId) = @{} - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptionsCount = 1 - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptions = $arrayExemptions - } - else { - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptionsCount += 1 - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptions = $arrayExemptions - } + $null = $arrayExemptions.Add($htPolicyAssignmentExemptions.($exemptionId).exemption) + } + } + if ($arrayExemptions.Count -gt 0) { + $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId) = @{ + exemptionsCount = $arrayExemptions.Count + exemptions = $arrayExemptions } } #endregion exemptions } - $endPolicyAssignmnetsUniqueRelations = Get-Date - Write-Host " PolicyAssignmnetsUniqueRelations processing duration: $((New-TimeSpan -Start $startPolicyAssignmnetsUniqueRelations -End $endPolicyAssignmnetsUniqueRelations).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyAssignmnetsUniqueRelations -End $endPolicyAssignmnetsUniqueRelations).TotalSeconds) seconds)" + $endPolicyAssignmentsUniqueRelations = Get-Date + Write-Host " PolicyAssignmentsUniqueRelations processing duration: $((New-TimeSpan -Start $startPolicyAssignmentsUniqueRelations -End $endPolicyAssignmentsUniqueRelations).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyAssignmentsUniqueRelations -End $endPolicyAssignmentsUniqueRelations).TotalSeconds) seconds)" #endregion PolicyAssignmentsUniqueRelations #region PolicyAssignmentsAllCreateEnriched @@ -19617,14 +19773,14 @@ extensions: [{ name: 'sort' }] #region SUMMARYMGdefault Write-Host ' processing TenantSummary ManagementGroups - default Management Group' [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

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

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

    +

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

    "@) #endregion SUMMARYMGRequireAuthorizationForGroupCreation @@ -19671,8 +19827,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Supported Microsoft Azure offers docs
    - Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
    + Supported Microsoft Azure offers learn
    + Understand Microsoft Defender for Cloud Secure Score Video , Blog , learn
    Download CSV semicolon | comma
    @@ -20074,11 +20230,11 @@ extensions: [{ name: 'sort' }] $tfCount = $tagsUsageCount $htmlTableId = 'TenantSummary_tagsUsage' [void]$htmlTenantSummary.AppendLine(@" - -
    - Resource naming and tagging decision guide docs
    - Download CSV semicolon | comma -
    + +
    + Resource naming and tagging decision guide learn
    + Download CSV semicolon | comma +
    @@ -20151,7 +20307,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

    Tag Name Usage ($tagsUsageCount Tags) docs

    +

    Tag Name Usage ($tagsUsageCount Tags) learn

    "@) } #endregion SUMMARYTagNameUsage @@ -20477,7 +20633,7 @@ extensions: [{ name: 'sort' }]
    - CAF - Recommended abbreviations for Azure resource types docs
    + CAF - Recommended abbreviations for Azure resource types learn
    Resource details can be found in the CSV output *_ResourcesAll.csv
    Download CSV semicolon | comma
    Scope
    @@ -21086,7 +21242,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Set up preview features in Azure subscription docs
    + Set up preview features in Azure subscription learn
    Download CSV semicolon | comma
    @@ -21163,7 +21319,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

    No enabled Subscriptions Features docs

    +

    No enabled Subscriptions Features learn

    '@) } $endSubFeatures = Get-Date @@ -21190,7 +21346,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Considerations before applying locks docs
    + Considerations before applying locks learn
    Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv
    @@ -21259,7 +21415,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

    No Resource Locks at all docs

    +

    No Resource Locks at all learn

    '@) } $endResourceLocks = Get-Date @@ -21279,8 +21435,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Register Resource Provider 'Microsoft.Security' docs
    - Microsoft Defender for Cloud's enhanced security features docs
    + Register Resource Provider 'Microsoft.Security' learn
    + Microsoft Defender for Cloud's enhanced security features learn
    Download CSV semicolon | comma
    @@ -21378,17 +21534,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
    + Using deprecated plan 'Container registries'learn
    '@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
    + Using deprecated plan 'Kubernetes'learn
    '@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
    + Microsoft Defender for Cloud's enhanced security featureslearn
    Download CSV semicolon | comma
    @@ -21460,17 +21616,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
    + Using deprecated plan 'Container registries'learn
    '@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
    + Using deprecated plan 'Kubernetes'learn
    '@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
    + Microsoft Defender for Cloud's enhanced security featureslearn
    Download CSV semicolon | comma
    @@ -21637,7 +21793,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Managed identity 'user-assigned' vs 'system-assigned' docs
    + Managed identity 'user-assigned' vs 'system-assigned' learn
    Download CSV semicolon | comma
    @@ -23102,7 +23258,7 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { [void]$htmlTenantSummary.AppendLine(@"
    - Management Group Diagnostic Settings - Create Or Update - REST API docs
    + Management Group Diagnostic Settings - Create Or Update - REST API learn
    Download CSV semicolon | comma
    @@ -23240,7 +23396,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    No Management Groups configured for Diagnostic settings docs

    +

    No Management Groups configured for Diagnostic settings learn

    '@) } @@ -23249,9 +23405,9 @@ extensions: [{ name: 'sort' }] $tfCount = $arrayMgsWithoutDiagnosticsCount $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' [void]$htmlTenantSummary.AppendLine(@" - +
    - Management Group Diagnostic Settings - Create Or Update - REST API docs
    + Management Group Diagnostic Settings - Create Or Update - REST API learn
    Download CSV semicolon | comma
    @@ -23326,7 +23482,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    All Management Groups are configured for Diagnostic settings docs

    +

    All Management Groups are configured for Diagnostic settings learn

    '@) } #endregion SUMMARYDiagnosticsManagementGroups @@ -23346,7 +23502,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Create diagnostic setting docs
    + Create diagnostic setting learn
    Download CSV semicolon | comma
    @@ -23480,7 +23636,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    No Subscriptions configured for Diagnostic settings docs

    +

    No Subscriptions configured for Diagnostic settings learn

    '@) } @@ -23491,7 +23647,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Create diagnostic setting docs
    + Create diagnostic setting learn
    Download CSV semicolon | comma
    @@ -23566,7 +23722,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    All Subscriptions are configured for Diagnostic settings docs

    +

    All Subscriptions are configured for Diagnostic settings learn

    '@) } #endregion SUMMARYDiagnosticsSubscriptions @@ -23593,7 +23749,7 @@ extensions: [{ name: 'sort' }]
    Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
    - Supported categories for Azure Resource Logs docs
    + Supported categories for Azure Resource Logs learn
    Download CSV semicolon | comma
    @@ -23917,7 +24073,7 @@ extensions: [{ name: 'sort' }] else { $resourceCount = '0' } - $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check learn " $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ Priority = '2-Medium' PolicyId = 'n/a' @@ -23952,7 +24108,7 @@ extensions: [{ name: 'sort' }]
    Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
    - Supported categories for Azure Resource Logs docs + Supported categories for Azure Resource Logs learn
    @@ -24129,24 +24285,24 @@ extensions: [{ name: 'sort' }] #policySets if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { [void]$htmlTenantSummary.AppendLine(@" -

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

    +

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant learn

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

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

    +

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant learn

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

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

    +

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant learn

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

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

    +

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant learn

    "@) } @@ -24166,7 +24322,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -24239,7 +24395,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsPolicyAssignments @@ -24253,7 +24409,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -24326,7 +24482,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsPolicyScope @@ -24340,7 +24496,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -24413,7 +24569,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsPolicySetScope @@ -24428,7 +24584,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure RBAC Limits docs
    + Azure RBAC Limits learn
    Download CSV semicolon | comma
    @@ -24501,7 +24657,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsRoleAssignment @@ -24522,7 +24678,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Subscription Resource Group Limit docs
    + Azure Subscription Resource Group Limit learn
    Download CSV semicolon | comma
    @@ -24596,7 +24752,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

    + $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups learn

    "@) } #endregion SUMMARYSubsapproachingLimitsResourceGroups @@ -24610,7 +24766,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Subscription Tag Limit docs
    + Azure Subscription Tag Limit learn
    Download CSV semicolon | comma
    @@ -24683,7 +24839,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsSubscriptionTags @@ -24697,7 +24853,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -24770,7 +24926,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsPolicyAssignments @@ -24784,7 +24940,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -24857,7 +25013,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsPolicyScope @@ -24871,7 +25027,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -24944,7 +25100,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsPolicySetScope @@ -24961,7 +25117,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure RBAC Limits docs
    + Azure RBAC Limits learn
    Download CSV semicolon | comma
    @@ -25034,7 +25190,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

    + $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment learn

    "@) } #endregion SUMMARYSubsapproachingLimitsRoleAssignment @@ -29748,15 +29904,19 @@ function dataCollectionResources { $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resources?`$expand=createdTime,changedTime,properties&api-version=2023-07-01" $method = 'GET' $resourcesSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' - #Write-Host 'arm resList count:'$resourcesSubscriptionResult.Count #endregion resources LIST #region resources GET if ($resourcesSubscriptionResult.Count -gt 0) { $arrayResourcesWithProperties = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) - $resourcesSubscriptionResult | ForEach-Object -Parallel { - $resource = $_ + $batchSize = [math]::ceiling($resourcesSubscriptionResult.Count / $azAPICallConf['htParameters'].ThrottleLimit) + #Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $resourcesSubscriptionResultBatch = ($resourcesSubscriptionResult) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + #Write-Host "Processing data in $($resourcesSubscriptionResultBatch.Count) batches" + + $resourcesSubscriptionResultBatch | ForEach-Object -Parallel { #region using $arrayResourcesWithProperties = $using:arrayResourcesWithProperties $htResourceProvidersRef = $using:htResourceProvidersRef @@ -29770,65 +29930,62 @@ function dataCollectionResources { #$htResourcesWithProperties = $using:htResourcesWithProperties #endregion using - if ($htAvailablePrivateEndpointTypes.(($resource.type).ToLower())) { - #Write-Host "$($resource.type) in `$htAvailablePrivateEndpointTypes" - if ($htResourceProvidersRef.($resource.type)) { - if ($htResourceProvidersRef.($resource.type).APIDefault) { - $apiVersionToUse = $htResourceProvidersRef.($resource.type).APIDefault - $apiRef = 'default' - } - else { - $apiVersionToUse = $htResourceProvidersRef.($resource.type).APILatest - $apiRef = 'latest' - } + foreach ($resource in $_.Group) { - $currentTask = "Getting Resource Properties API-version: '$apiVersionToUse' ($apiRef); ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)'" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($resource.id)?api-version=$apiVersionToUse" - $method = 'GET' - $resourceResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -listenOn Content -unhandledErrorAction Continue - - if ($resourceResult -ne 'ResourceOrResourcegroupNotFound' -and $resourceResult -ne 'convertfromJSONError') { - $null = $script:arrayResourcesWithProperties.Add($resourceResult) - #$script:htResourcesWithProperties.($resourceResult.id) = $resourceResult - if ($resourceResult.properties.privateEndpointConnections.Count -gt 0) { - foreach ($privateEndpointConnection in $resourceResult.properties.privateEndpointConnections) { - $resourceResultIdSplit = $resourceResult.id -split '/' - $null = $script:arrayPrivateEndPointsFromResourceProperties.Add([PSCustomObject]@{ - ResourceName = $resourceResult.name - ResourceType = $resourceResult.type - ResourceId = $resourceResult.id - ResourceResourceGroup = $resourceResultIdSplit[4] - ResourceSubscriptionId = $scopeId - ResourceSubscriptionName = $scopeDisplayName - ResourceMGPath = $ChildMgParentNameChainDelimited - privateEndpointConnection = $privateEndpointConnection - }) + if ($htAvailablePrivateEndpointTypes.(($resource.type).ToLower())) { + if ($htResourceProvidersRef.($resource.type)) { + if ($htResourceProvidersRef.($resource.type).APIDefault) { + $apiVersionToUse = $htResourceProvidersRef.($resource.type).APIDefault + $apiRef = 'default' + } + else { + $apiVersionToUse = $htResourceProvidersRef.($resource.type).APILatest + $apiRef = 'latest' + } + + $currentTask = "Getting Resource Properties API-version: '$apiVersionToUse' ($apiRef); ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($resource.id)?api-version=$apiVersionToUse" + $method = 'GET' + $resourceResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -listenOn Content -unhandledErrorAction Continue + + if ($resourceResult -ne 'ResourceOrResourcegroupNotFound' -and $resourceResult -ne 'convertfromJSONError') { + $null = $script:arrayResourcesWithProperties.Add($resourceResult) + #$script:htResourcesWithProperties.($resourceResult.id) = $resourceResult + if ($resourceResult.properties.privateEndpointConnections.Count -gt 0) { + foreach ($privateEndpointConnection in $resourceResult.properties.privateEndpointConnections) { + $resourceResultIdSplit = $resourceResult.id -split '/' + $null = $script:arrayPrivateEndPointsFromResourceProperties.Add([PSCustomObject]@{ + ResourceName = $resourceResult.name + ResourceType = $resourceResult.type + ResourceId = $resourceResult.id + ResourceResourceGroup = $resourceResultIdSplit[4] + ResourceSubscriptionId = $scopeId + ResourceSubscriptionName = $scopeDisplayName + ResourceMGPath = $ChildMgParentNameChainDelimited + privateEndpointConnection = $privateEndpointConnection + }) + } + } + } + else { + if ($resourceResult -eq 'convertfromJSONError') { + $script:htResourcePropertiesConvertfromJSONFailed.($resource.id) = @{} } } } else { - if ($resourceResult -eq 'convertfromJSONError') { - $script:htResourcePropertiesConvertfromJSONFailed.($resource.id) = @{} - } + Write-Host "[Azure Governance Visualizer] Please file an issue at the Azure Governance Visualizer GitHub repository (aka.ms/AzGovViz) and provide this information (scrub subscription Id and company identifyable names): No API-version matches! ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)' - Thank you!" -ForegroundColor DarkRed } } else { - Write-Host "[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 + #Write-Host "$($resource.type) not in `$htAvailablePrivateEndpointTypes" } } - else { - #Write-Host "$($resource.type) not in `$htAvailablePrivateEndpointTypes" - } } -ThrottleLimit $azAPICallConf['htParameters'].ThrottleLimit } - #Write-Host 'arm resGet count:' $arrayResourcesWithProperties.Count #endregion resources GET - # if ($resourcesSubscriptionResult.Count -ne $arrayResourcesWithProperties.Count) { - # Write-Host " FYI: Getting Resources for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId'] - ARM list count: $($resourcesSubscriptionResult.Count); ARG get count: $($arrayResourcesWithProperties.Count)" - # } - #region PSRule if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { if ($resourcesSubscriptionResult.Count -gt 0) { @@ -30491,7 +30648,6 @@ function dataCollectionResources { foreach ($naming in $namingConvention) { if (($resource.name).StartsWith($naming, 'CurrentCultureIgnoreCase')) { $cafResourceNamingCheck = 'passed' - #$applicableNaming = $naming } } } @@ -30596,7 +30752,6 @@ function dataCollectionResourceGroups { $subscriptionQuotaId ) - #https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups?api-version=2020-06-01 $currentTask = "Getting ResourceGroups for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resourcegroups?api-version=2021-04-01" $method = 'GET' @@ -31431,12 +31586,11 @@ function dataCollectionPolicyDefinitions { if (-not [string]::IsNullOrEmpty($roledefinitionId)) { if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$hlpPolicyDefinitionId + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [System.Collections.ArrayList]@() + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($hlpPolicyDefinitionId) } else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $hlpPolicyDefinitionId - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($hlpPolicyDefinitionId) } } else { @@ -32014,7 +32168,6 @@ function dataCollectionPolicyAssignmentsMG { $policySetCategory = $policySetDefinition.Category } else { - #test #Write-Host "pa '($L0mgmtGroupPolicyAssignment.Id)' scope: '$($scopeId)' - policySetDefinition not available: $policySetDefinitionId" Start-Sleep -Seconds 1 } @@ -32705,11 +32858,11 @@ function dataCollectionRoleDefinitions { if ($TargetMgOrSub -eq 'Sub') { $currentTask = "Getting Custom Role definitions for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-07-01&`$filter=type eq 'CustomRole'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'CustomRole'" } if ($TargetMgOrSub -eq 'MG') { $currentTask = "Getting Custom Role definitions for Management Group: '$($scopeDisplayName)' ('$scopeId')" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-07-01&`$filter=type eq 'CustomRole'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'CustomRole'" } $method = 'GET' $scopeCustomRoleDefinitions = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -32795,7 +32948,6 @@ function dataCollectionRoleAssignmentsMG { $roleAssignmentScheduleInstances = ($roleAssignmentScheduleInstancesFromAPI.where( { ($_.properties.roleAssignmentScheduleId -replace '.*/') -ne ($_.properties.originRoleAssignmentId -replace '.*/') })) $roleAssignmentScheduleInstancesCount = $roleAssignmentScheduleInstances.Count if ($roleAssignmentScheduleInstancesCount -gt 0) { - #$htRoleAssignmentsPIM = @{} foreach ($roleAssignmentScheduleInstance in $roleAssignmentScheduleInstances) { $script:htRoleAssignmentsPIM.($roleAssignmentScheduleInstance.properties.originRoleAssignmentId.tolower()) = $roleAssignmentScheduleInstance.properties } @@ -33077,7 +33229,6 @@ function dataCollectionRoleAssignmentsSub { $roleAssignmentScheduleInstances = ($roleAssignmentScheduleInstancesFromAPI.where( { ($_.properties.roleAssignmentScheduleId -replace '.*/') -ne ($_.properties.originRoleAssignmentId -replace '.*/') })) $roleAssignmentScheduleInstancesCount = $roleAssignmentScheduleInstances.Count if ($roleAssignmentScheduleInstancesCount -gt 0) { - #$htRoleAssignmentsPIM = @{} foreach ($roleAssignmentScheduleInstance in $roleAssignmentScheduleInstances) { $script:htRoleAssignmentsPIM.($roleAssignmentScheduleInstance.properties.originRoleAssignmentId.tolower()) = $roleAssignmentScheduleInstance.properties } @@ -33672,7 +33823,7 @@ function processScopeInsights($mgChild, $mgChildOf) { "@ if ($mgId -eq $defaultManagementGroupId) { $script:html += @' -
    + '@ } $script:html += @" @@ -34230,36 +34381,83 @@ if (-not $HierarchyMapOnly) { #region Getting Available Private Endpoint Types $startGetAvailablePrivateEndpointTypes = Get-Date - $currentTask = 'Getting Locations' - Write-Host $currentTask - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($azAPICallConf['checkcontext'].Subscription.Id)/locations?api-version=2020-01-01" - $method = 'GET' - $getLocations = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - Write-Host " Returned $($getLocations.Count) locations" + $subsToProcessForGettingPrivateEndpointTypes = [System.Collections.ArrayList]@() + $prioCounter = 0 + foreach ($subscription in $subsToProcessInCustomDataCollection) { + $prioCounter++ + if ($subscription.subscriptionId -eq $azAPICallConf['checkcontext'].Subscription.Id) { + $null = $subsToProcessForGettingPrivateEndpointTypes.Add([PSCustomObject]@{ + subscriptionInfo = $subscription + prio = 0 + }) + } + else { + $null = $subsToProcessForGettingPrivateEndpointTypes.Add([PSCustomObject]@{ + subscriptionInfo = $subscription + prio = $prioCounter + }) + } + } - Write-Host "Getting 'Available Private Endpoint Types' for $($getLocations.Count) locations" - $getLocations | ForEach-Object -Parallel { - $location = $_ - $azAPICallConf = $using:azAPICallConf - $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes - $currentTask = "Getting 'Available Private Endpoint Types' for location $($location.name)" - #Write-Host $currentTask - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($azAPICallConf['checkcontext'].Subscription.Id)/providers/Microsoft.Network/locations/$($location.name)/availablePrivateEndpointTypes?api-version=2022-07-01" + foreach ($subscription in $subsToProcessForGettingPrivateEndpointTypes | Sort-Object -Property prio) { + + if ($privateEndpointAvailabilityCheckCompleted) { + continue + } + + $subscriptionId = $subscription.subscriptionInfo.subscriptionId + $subscriptionName = $subscription.subscriptionInfo.subscriptionName + + $currentTask = "Getting Locations for Subscription '$($subscriptionName)' ($($subscriptionId))" + Write-Host $currentTask + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subscriptionId)/locations?api-version=2020-01-01" $method = 'GET' - $availablePrivateEndpointTypes = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -skipOnErrorCode 400, 409 - Write-Host " Returned $($availablePrivateEndpointTypes.Count) 'Available Private Endpoint Types' for location $($location.name)" - foreach ($availablePrivateEndpointType in $availablePrivateEndpointTypes) { - if (-not $htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower())) { - $script:htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower()) = @{} + $getLocations = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + Write-Host " Returned $($getLocations.Count) locations" + + Write-Host "Getting 'Available Private Endpoint Types' for Subscription '$($subscriptionName)' ($($subscriptionId)) for $($getLocations.Count) locations" + + $batchSize = [math]::ceiling($getLocations.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $getLocationsBatch = ($getLocations) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($getLocationsBatch.Count) batches" + + $getLocationsBatch | ForEach-Object -Parallel { + $subscriptionId = $using:subscriptionId + $azAPICallConf = $using:azAPICallConf + $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes + + foreach ($location in $_.Group) { + $currentTask = "Getting 'Available Private Endpoint Types' for location $($location.name)" + #Write-Host $currentTask + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subscriptionId)/providers/Microsoft.Network/locations/$($location.name)/availablePrivateEndpointTypes?api-version=2022-07-01" + $method = 'GET' + $availablePrivateEndpointTypes = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -skipOnErrorCode 400, 409 + Write-Host " Returned $($availablePrivateEndpointTypes.Count) 'Available Private Endpoint Types' for location $($location.name)" + foreach ($availablePrivateEndpointType in $availablePrivateEndpointTypes) { + if (-not $htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower())) { + $script:htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower()) = @{} + } + } } + } -ThrottleLimit $ThrottleLimit + + if ($htAvailablePrivateEndpointTypes.Keys.Count -gt 0) { + #Write-Host " Created ht for $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types'" + $privateEndpointAvailabilityCheckCompleted = $true } - } -ThrottleLimit $ThrottleLimit + else { + Write-Host " $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types' - likely the Resource Provider 'Microsoft.Network' is not registered - trying next available subscription" + $privateEndpointAvailabilityCheckCompleted = $false + } + } if ($htAvailablePrivateEndpointTypes.Keys.Count -gt 0) { Write-Host " Created ht for $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types'" } else { - $throwmsg = "$($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types' - Please use another Subscription for the AzContext (current subscriptionId: '$($azAPICallConf['checkcontext'].Subscription.Id)') -> use parameter: -SubscriptionId4AzContext ''" + $throwmsg = "$($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types' - Checked for $($subsToProcessForGettingPrivateEndpointTypes.Count) Subscriptions with no success. Make sure that for at least one Subscription the Resource Provider 'Microsoft.Network' is registered. Once you registered the Resource Provider for Subscription 'subscriptionEnabled' it may be a good idea to use the parameter: -SubscriptionId4AzContext ''" Write-Host $throwmsg -ForegroundColor DarkRed Throw $throwmsg } diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index 7a212338..b34c55f2 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -365,14 +365,14 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.3.71', + $ProductVersion = '6.4.0', [string] $GithubRepository = 'aka.ms/AzGovViz', # <--- AzAPICall related parameters #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall [string] - $AzAPICallVersion = '1.1.85', + $AzAPICallVersion = '1.2.0', [switch] $DebugAzAPICall, @@ -1156,36 +1156,83 @@ if (-not $HierarchyMapOnly) { #region Getting Available Private Endpoint Types $startGetAvailablePrivateEndpointTypes = Get-Date - $currentTask = 'Getting Locations' - Write-Host $currentTask - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($azAPICallConf['checkcontext'].Subscription.Id)/locations?api-version=2020-01-01" - $method = 'GET' - $getLocations = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - Write-Host " Returned $($getLocations.Count) locations" - - Write-Host "Getting 'Available Private Endpoint Types' for $($getLocations.Count) locations" - $getLocations | ForEach-Object -Parallel { - $location = $_ - $azAPICallConf = $using:azAPICallConf - $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes - $currentTask = "Getting 'Available Private Endpoint Types' for location $($location.name)" - #Write-Host $currentTask - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($azAPICallConf['checkcontext'].Subscription.Id)/providers/Microsoft.Network/locations/$($location.name)/availablePrivateEndpointTypes?api-version=2022-07-01" + $subsToProcessForGettingPrivateEndpointTypes = [System.Collections.ArrayList]@() + $prioCounter = 0 + foreach ($subscription in $subsToProcessInCustomDataCollection) { + $prioCounter++ + if ($subscription.subscriptionId -eq $azAPICallConf['checkcontext'].Subscription.Id) { + $null = $subsToProcessForGettingPrivateEndpointTypes.Add([PSCustomObject]@{ + subscriptionInfo = $subscription + prio = 0 + }) + } + else { + $null = $subsToProcessForGettingPrivateEndpointTypes.Add([PSCustomObject]@{ + subscriptionInfo = $subscription + prio = $prioCounter + }) + } + } + + foreach ($subscription in $subsToProcessForGettingPrivateEndpointTypes | Sort-Object -Property prio) { + + if ($privateEndpointAvailabilityCheckCompleted) { + continue + } + + $subscriptionId = $subscription.subscriptionInfo.subscriptionId + $subscriptionName = $subscription.subscriptionInfo.subscriptionName + + $currentTask = "Getting Locations for Subscription '$($subscriptionName)' ($($subscriptionId))" + Write-Host $currentTask + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subscriptionId)/locations?api-version=2020-01-01" $method = 'GET' - $availablePrivateEndpointTypes = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -skipOnErrorCode 400, 409 - Write-Host " Returned $($availablePrivateEndpointTypes.Count) 'Available Private Endpoint Types' for location $($location.name)" - foreach ($availablePrivateEndpointType in $availablePrivateEndpointTypes) { - if (-not $htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower())) { - $script:htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower()) = @{} + $getLocations = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + Write-Host " Returned $($getLocations.Count) locations" + + Write-Host "Getting 'Available Private Endpoint Types' for Subscription '$($subscriptionName)' ($($subscriptionId)) for $($getLocations.Count) locations" + + $batchSize = [math]::ceiling($getLocations.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $getLocationsBatch = ($getLocations) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($getLocationsBatch.Count) batches" + + $getLocationsBatch | ForEach-Object -Parallel { + $subscriptionId = $using:subscriptionId + $azAPICallConf = $using:azAPICallConf + $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes + + foreach ($location in $_.Group) { + $currentTask = "Getting 'Available Private Endpoint Types' for location $($location.name)" + #Write-Host $currentTask + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subscriptionId)/providers/Microsoft.Network/locations/$($location.name)/availablePrivateEndpointTypes?api-version=2022-07-01" + $method = 'GET' + $availablePrivateEndpointTypes = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -skipOnErrorCode 400, 409 + Write-Host " Returned $($availablePrivateEndpointTypes.Count) 'Available Private Endpoint Types' for location $($location.name)" + foreach ($availablePrivateEndpointType in $availablePrivateEndpointTypes) { + if (-not $htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower())) { + $script:htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower()) = @{} + } + } } + } -ThrottleLimit $ThrottleLimit + + if ($htAvailablePrivateEndpointTypes.Keys.Count -gt 0) { + #Write-Host " Created ht for $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types'" + $privateEndpointAvailabilityCheckCompleted = $true + } + else { + Write-Host " $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types' - likely the Resource Provider 'Microsoft.Network' is not registered - trying next available subscription" + $privateEndpointAvailabilityCheckCompleted = $false } - } -ThrottleLimit $ThrottleLimit + } if ($htAvailablePrivateEndpointTypes.Keys.Count -gt 0) { Write-Host " Created ht for $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types'" } else { - $throwmsg = "$($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types' - Please use another Subscription for the AzContext (current subscriptionId: '$($azAPICallConf['checkcontext'].Subscription.Id)') -> use parameter: -SubscriptionId4AzContext ''" + $throwmsg = "$($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types' - Checked for $($subsToProcessForGettingPrivateEndpointTypes.Count) Subscriptions with no success. Make sure that for at least one Subscription the Resource Provider 'Microsoft.Network' is registered. Once you registered the Resource Provider for Subscription 'subscriptionEnabled' it may be a good idea to use the parameter: -SubscriptionId4AzContext ''" Write-Host $throwmsg -ForegroundColor DarkRed Throw $throwmsg } diff --git a/pwsh/dev/functions/buildJSON.ps1 b/pwsh/dev/functions/buildJSON.ps1 index 8166d335..71b7e65f 100644 --- a/pwsh/dev/functions/buildJSON.ps1 +++ b/pwsh/dev/functions/buildJSON.ps1 @@ -32,9 +32,11 @@ function buildJSON { $htSubRGPolicyAssignments.($subId) = @{} } if (-not $htSubRGPolicyAssignments.($subId).PolicyAssignments) { - $htSubRGPolicyAssignments.($subId).PolicyAssignments = @() + $htSubRGPolicyAssignments.($subId).PolicyAssignments = [System.Collections.ArrayList]@() + } + foreach ($rgpafg in $rgpa.group) { + $null = $htSubRGPolicyAssignments.($subId).PolicyAssignments.Add($rgpafg) } - $htSubRGPolicyAssignments.($subId).PolicyAssignments += $rgpa.group } } } @@ -54,9 +56,11 @@ function buildJSON { $htSubRGRoleAssignments.($subId) = @{} } if (-not $htSubRGRoleAssignments.($subId).RoleAssignments) { - $htSubRGRoleAssignments.($subId).RoleAssignments = @() + $htSubRGRoleAssignments.($subId).RoleAssignments = [System.Collections.ArrayList]@() + } + foreach ($rgrafg in $rgra.group) { + $null = $htSubRGRoleAssignments.($subId).RoleAssignments.Add($rgrafg) } - $htSubRGRoleAssignments.($subId).RoleAssignments += $rgra.group } #res @@ -307,6 +311,9 @@ function buildJSON { } if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)") { + if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Definitions") { + $createDefinitionsLegacyAndNew = $true + } Write-Host ' Cleaning old state (Pipeline only)' Remove-Item -Recurse -Force "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)" } @@ -328,6 +335,7 @@ function buildJSON { } $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Definitions" -ItemType directory -Path $outputPath + $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking" -ItemType directory -Path $outputPath @@ -341,16 +349,47 @@ function buildJSON { $null = New-Item -Name "$($pathRoleDefinitionCustom)" -ItemType directory -Path $outputPath $null = New-Item -Name "$($pathRoleDefinitionBuiltIn)" -ItemType directory -Path $outputPath } + $pathRoleDefinitionsTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)RoleDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionsTracking)")) { + $null = New-Item -Name $pathRoleDefinitionsTracking -ItemType directory -Path $outputPath + $pathRoleDefinitionCustomTracking = "$($pathRoleDefinitionsTracking)$($DirectorySeparatorChar)Custom" + $pathRoleDefinitionBuiltInTracking = "$($pathRoleDefinitionsTracking)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathRoleDefinitionCustomTracking)" -ItemType directory -Path $outputPath + $null = New-Item -Name "$($pathRoleDefinitionBuiltInTracking)" -ItemType directory -Path $outputPath + } if (($htCacheDefinitionsRole).Keys.Count -gt 0) { foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { ($htCacheDefinitionsRole).($_).IsCustom }) | Sort-Object) { $htJSON.RoleDefinitions.($roleDefinition) = ($htCacheDefinitionsRole).($roleDefinition).Json.properties $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionCustom)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 + + #if a custom role has multiple assignable scopes, the definition id may vary depending which scope AzGovViz retrieved the definition from, therefore for better change tracking we pack assignablescopes, sort them and use the first entry as id + + if (($htCacheDefinitionsRole).($roleDefinition).Json.properties.assignableScopes.Count -gt 1) { + $jsonAdjustment4Tracking = (($htCacheDefinitionsRole).($roleDefinition).Json).psobject.copy() + $arrayAssignableScopes = [System.Collections.ArrayList]@() + foreach ($assignableScope in $jsonAdjustment4Tracking.properties.assignableScopes) { + if ($assignableScope -like '/subscriptions/*') { + $null = $arrayAssignableScopes.Add("$($assignableScope)/providers/Microsoft.Authorization/roleDefinitions/$($jsonAdjustment4Tracking.name)") + } + else { + $null = $arrayAssignableScopes.Add("/providers/Microsoft.Authorization/roleDefinitions/$($jsonAdjustment4Tracking.name)") + } + } + $jsonAdjustment4Tracking.id = ($arrayAssignableScopes | Sort-Object)[0] + $jsonConvertedTracking = $jsonAdjustment4Tracking | ConvertTo-Json -Depth 99 + } + else { + $jsonConvertedTracking = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionCustomTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsRole).($roleDefinition).Id).json" -Encoding utf8 } foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { -not ($htCacheDefinitionsRole).($_).IsCustom })) { - $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name ) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 + $jsonConvertedTracking = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionBuiltInTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsRole).($roleDefinition).Id).json" -Encoding utf8 } } @@ -360,10 +399,18 @@ function buildJSON { $pathPolicyDefinitionBuiltIn = "$($pathPolicyDefinitions)$($DirectorySeparatorChar)BuiltIn" $null = New-Item -Name "$($pathPolicyDefinitionBuiltIn)" -ItemType directory -Path $outputPath } + $pathPolicyDefinitionsTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicyDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionsTracking)")) { + $null = New-Item -Name $pathPolicyDefinitionsTracking -ItemType directory -Path $outputPath + $pathPolicyDefinitionBuiltInTracking = "$($pathPolicyDefinitionsTracking)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathPolicyDefinitionBuiltInTracking)" -ItemType directory -Path $outputPath + } if (($htCacheDefinitionsPolicy).Keys.Count -gt 0) { foreach ($policyDefinition in ($htCacheDefinitionsPolicy).Keys.where( { ($htCacheDefinitionsPolicy).($_).Type -eq 'BuiltIn' })) { $jsonConverted = ($htCacheDefinitionsPolicy).($policyDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicy).($policyDefinition).displayName) ($(($htCacheDefinitionsPolicy).($policyDefinition).Json.name)).json" -Encoding utf8 + $jsonConvertedTracking = ($htCacheDefinitionsPolicy).($policyDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionBuiltInTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsPolicy).($policyDefinition).Json.name).json" -Encoding utf8 } } @@ -373,10 +420,18 @@ function buildJSON { $pathPolicySetDefinitionBuiltIn = "$($pathPolicySetDefinitions)$($DirectorySeparatorChar)BuiltIn" $null = New-Item -Name "$($pathPolicySetDefinitionBuiltIn)" -ItemType directory -Path $outputPath } + $pathPolicySetDefinitionsTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicySetDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionsTracking)")) { + $null = New-Item -Name $pathPolicySetDefinitionsTracking -ItemType directory -Path $outputPath + $pathPolicySetDefinitionBuiltInTracking = "$($pathPolicySetDefinitionsTracking)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathPolicySetDefinitionBuiltInTracking)" -ItemType directory -Path $outputPath + } if (($htCacheDefinitionsPolicySet).Keys.Count -gt 0) { foreach ($policySetDefinition in ($htCacheDefinitionsPolicySet).Keys.where( { ($htCacheDefinitionsPolicySet).($_).Type -eq 'BuiltIn' })) { $jsonConverted = ($htCacheDefinitionsPolicySet).($policySetDefinition).Json.properties | ConvertTo-Json -Depth 99 $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicySet).($policySetDefinition).displayName) ($(($htCacheDefinitionsPolicySet).($policySetDefinition).Json.name)).json" -Encoding utf8 + $jsonConverted = ($htCacheDefinitionsPolicySet).($policySetDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionBuiltInTracking)$($DirectorySeparatorChar)$(($htCacheDefinitionsPolicySet).($policySetDefinition).Json.name).json" -Encoding utf8 } } @@ -411,6 +466,12 @@ function buildJSON { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Tenant" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 } $htTree.'Tenant'.'ManagementGroups' = [ordered] @{} @@ -419,6 +480,9 @@ function buildJSON { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Assignments")) { $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Assignments" -ItemType directory -Path $outputPath } + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking")) { + $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking" -ItemType directory -Path $outputPath + } buildTree -mgId $ManagementGroupId -json $json -prnt "$($JSONPath)$($DirectorySeparatorChar)Tenant" diff --git a/pwsh/dev/functions/buildTree.ps1 b/pwsh/dev/functions/buildTree.ps1 index 4121ea75..d75fc5d9 100644 --- a/pwsh/dev/functions/buildTree.ps1 +++ b/pwsh/dev/functions/buildTree.ps1 @@ -12,12 +12,13 @@ function buildTree($mgId, $prnt) { $json.'ManagementGroups' = [ordered]@{} } $json = $json.'ManagementGroups'.($getMg.Id) = [ordered]@{} - foreach ($mgCap in $htJSON.ManagementGroups.($getMg.Id).keys) { - $json.$mgCap = $htJSON.ManagementGroups.($getMg.Id).$mgCap + $mgJson = $htJSON.ManagementGroups.($getMg.Id) + foreach ($mgCap in $mgJson.keys) { + $json.$mgCap = $mgJson.$mgCap if ($mgCap -eq 'PolicyDefinitionsCustom') { $mgCapShort = 'pd' - foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pdc) + foreach ($pdc in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($pdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -31,12 +32,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($mgCap -eq 'PolicySetDefinitionsCustom') { $mgCapShort = 'psd' - foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($psdc) + foreach ($psdc in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($psdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -50,12 +58,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($mgCap -eq 'PolicyAssignments') { $mgCapShort = 'pa' - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pa) + foreach ($pa in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($pa) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -68,15 +83,20 @@ function buildTree($mgId, $prnt) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } - $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Assignments_tracking$($DirectorySeparatorChar)$($mgCap)$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } #marker if ($mgCap -eq 'RoleAssignments') { $mgCapShort = 'ra' - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($ra) + foreach ($ra in $mgJson.($mgCap).Keys) { + $hlp = $mgJson.($mgCap).($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } @@ -94,15 +114,15 @@ function buildTree($mgId, $prnt) { } if ($mgCap -eq 'Subscriptions') { - foreach ($sub in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { - $subNameValid = removeInvalidFileNameChars $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).SubscriptionName + foreach ($sub in $mgJson.($mgCap).Keys) { + $subNameValid = removeInvalidFileNameChars $mgJson.($mgCap).($sub).SubscriptionName $subFolderName = "$($prntx)$($DirectorySeparatorChar)$($subNameValid) ($($sub))" $null = New-Item -Name $subFolderName -ItemType directory -Path $outputPath - foreach ($subCap in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).Keys) { + foreach ($subCap in $mgJson.($mgCap).($sub).Keys) { if ($subCap -eq 'PolicyDefinitionsCustom') { $subCapShort = 'pd' - foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pdc) + foreach ($pdc in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($pdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -116,12 +136,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($sub)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($subCap -eq 'PolicySetDefinitionsCustom') { $subCapShort = 'psd' - foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($psdc) + foreach ($psdc in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($psdc) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -135,12 +162,19 @@ function buildTree($mgId, $prnt) { $null = New-Item -Name $path -ItemType directory -Path $outputPath } $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + + $jsonConvertedTracking = $hlp | ConvertTo-Json -Depth 99 + $pathTracking = "$($JSONPath)$($DirectorySeparatorChar)Definitions_tracking$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($sub)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)")) { + $null = New-Item -Name $pathTracking -ItemType directory -Path $outputPath + } + $jsonConvertedTracking | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathTracking)$($DirectorySeparatorChar)$(removeInvalidFileNameChars $hlp.name).json" -Encoding utf8 } } if ($subCap -eq 'PolicyAssignments') { $subCapShort = 'pa' - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pa) + foreach ($pa in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($pa) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -159,8 +193,8 @@ function buildTree($mgId, $prnt) { #marker if ($subCap -eq 'RoleAssignments') { $subCapShort = 'ra' - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($ra) + foreach ($ra in $mgJson.($mgCap).($sub).($subCap).Keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } @@ -181,12 +215,12 @@ function buildTree($mgId, $prnt) { if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { if (-not $JsonExportExcludeResourceGroups) { if ($subCap -eq 'ResourceGroups') { - foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { + foreach ($rg in $mgJson.($mgCap).($sub).($subCap).Keys | Sort-Object) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" } - foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.($pa) + foreach ($pa in $mgJson.($mgCap).($sub).($subCap).($rg).PolicyAssignments.keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($rg).PolicyAssignments.($pa) if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { $displayName = 'noDisplayNameGiven' } @@ -211,12 +245,12 @@ function buildTree($mgId, $prnt) { if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { if (-not $JsonExportExcludeResourceGroups) { if ($subCap -eq 'ResourceGroups') { - foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { + foreach ($rg in $mgJson.($mgCap).($sub).($subCap).Keys | Sort-Object) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" } - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.($ra) + foreach ($ra in $mgJson.($mgCap).($sub).($subCap).($rg).RoleAssignments.keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($rg).RoleAssignments.($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } @@ -234,12 +268,12 @@ function buildTree($mgId, $prnt) { #res if (-not $JsonExportExcludeResources) { - foreach ($res in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.keys) { + foreach ($res in $mgJson.($mgCap).($sub).($subCap).($rg).Resources.keys) { if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)")) { $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)" -ItemType directory -Path "$($outputPath)" } - foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.keys) { - $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.($ra) + foreach ($ra in $mgJson.($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.keys) { + $hlp = $mgJson.($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.($ra) if ($hlp.PIM -eq 'true') { $pim = 'PIM_' } diff --git a/pwsh/dev/functions/cacheBuiltIn.ps1 b/pwsh/dev/functions/cacheBuiltIn.ps1 index 051626d0..26129509 100644 --- a/pwsh/dev/functions/cacheBuiltIn.ps1 +++ b/pwsh/dev/functions/cacheBuiltIn.ps1 @@ -79,12 +79,11 @@ function cacheBuiltIn { foreach ($roledefinitionId in $builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$builtinPolicyDefinition.Id + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [System.Collections.ArrayList]@() + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($builtinPolicyDefinition.Id) } else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $builtinPolicyDefinition.Id - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($builtinPolicyDefinition.Id) } } } @@ -168,12 +167,11 @@ function cacheBuiltIn { foreach ($roledefinitionId in $staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$staticPolicyDefinition.Id + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [System.Collections.ArrayList]@() + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($staticPolicyDefinition.Id) } else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $staticPolicyDefinition.Id - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($staticPolicyDefinition.Id) } } } @@ -243,13 +241,11 @@ function cacheBuiltIn { $currentTask = 'Caching built-in Role definitions' Write-Host " $currentTask" $uri = "$($azAPICallConf['azAPIEndpointUrls'].'ARM')/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" } else { $currentTask = "Caching built-in Role definitions (Location: '$($ARMLocation)')" Write-Host " $currentTask" $uri = "$($azAPICallConf['azAPIEndpointUrls']."ARM$($ARMLocation)")/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" - #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" } $method = 'GET' @@ -289,7 +285,7 @@ function cacheBuiltIn { ($script:htCacheDefinitionsRole).($roleDefinition.name).NotActions = ($roleDefinition.properties.permissions.notActions) ($script:htCacheDefinitionsRole).($roleDefinition.name).DataActions = ($roleDefinition.properties.permissions.dataActions) ($script:htCacheDefinitionsRole).($roleDefinition.name).NotDataActions = ($roleDefinition.properties.permissions.notDataActions) - ($script:htCacheDefinitionsRole).($roleDefinition.name).Json = ($roleDefinition.properties) + ($script:htCacheDefinitionsRole).($roleDefinition.name).Json = $roleDefinition ($script:htCacheDefinitionsRole).($roleDefinition.name).LinkToAzAdvertizer = "$($roleDefinition.properties.roleName)" ($script:htCacheDefinitionsRole).($roleDefinition.name).RoleCanDoRoleAssignments = $roleCapable4RoleAssignmentsWrite } diff --git a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 index 4a789115..8350acc1 100644 --- a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 +++ b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 @@ -510,15 +510,19 @@ function dataCollectionResources { $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resources?`$expand=createdTime,changedTime,properties&api-version=2023-07-01" $method = 'GET' $resourcesSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' - #Write-Host 'arm resList count:'$resourcesSubscriptionResult.Count #endregion resources LIST #region resources GET if ($resourcesSubscriptionResult.Count -gt 0) { $arrayResourcesWithProperties = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) - $resourcesSubscriptionResult | ForEach-Object -Parallel { - $resource = $_ + $batchSize = [math]::ceiling($resourcesSubscriptionResult.Count / $azAPICallConf['htParameters'].ThrottleLimit) + #Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $resourcesSubscriptionResultBatch = ($resourcesSubscriptionResult) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + #Write-Host "Processing data in $($resourcesSubscriptionResultBatch.Count) batches" + + $resourcesSubscriptionResultBatch | ForEach-Object -Parallel { #region using $arrayResourcesWithProperties = $using:arrayResourcesWithProperties $htResourceProvidersRef = $using:htResourceProvidersRef @@ -532,65 +536,62 @@ function dataCollectionResources { #$htResourcesWithProperties = $using:htResourcesWithProperties #endregion using - if ($htAvailablePrivateEndpointTypes.(($resource.type).ToLower())) { - #Write-Host "$($resource.type) in `$htAvailablePrivateEndpointTypes" - if ($htResourceProvidersRef.($resource.type)) { - if ($htResourceProvidersRef.($resource.type).APIDefault) { - $apiVersionToUse = $htResourceProvidersRef.($resource.type).APIDefault - $apiRef = 'default' - } - else { - $apiVersionToUse = $htResourceProvidersRef.($resource.type).APILatest - $apiRef = 'latest' - } + foreach ($resource in $_.Group) { + + if ($htAvailablePrivateEndpointTypes.(($resource.type).ToLower())) { + if ($htResourceProvidersRef.($resource.type)) { + if ($htResourceProvidersRef.($resource.type).APIDefault) { + $apiVersionToUse = $htResourceProvidersRef.($resource.type).APIDefault + $apiRef = 'default' + } + else { + $apiVersionToUse = $htResourceProvidersRef.($resource.type).APILatest + $apiRef = 'latest' + } - $currentTask = "Getting Resource Properties API-version: '$apiVersionToUse' ($apiRef); ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)'" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($resource.id)?api-version=$apiVersionToUse" - $method = 'GET' - $resourceResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -listenOn Content -unhandledErrorAction Continue - - if ($resourceResult -ne 'ResourceOrResourcegroupNotFound' -and $resourceResult -ne 'convertfromJSONError') { - $null = $script:arrayResourcesWithProperties.Add($resourceResult) - #$script:htResourcesWithProperties.($resourceResult.id) = $resourceResult - if ($resourceResult.properties.privateEndpointConnections.Count -gt 0) { - foreach ($privateEndpointConnection in $resourceResult.properties.privateEndpointConnections) { - $resourceResultIdSplit = $resourceResult.id -split '/' - $null = $script:arrayPrivateEndPointsFromResourceProperties.Add([PSCustomObject]@{ - ResourceName = $resourceResult.name - ResourceType = $resourceResult.type - ResourceId = $resourceResult.id - ResourceResourceGroup = $resourceResultIdSplit[4] - ResourceSubscriptionId = $scopeId - ResourceSubscriptionName = $scopeDisplayName - ResourceMGPath = $ChildMgParentNameChainDelimited - privateEndpointConnection = $privateEndpointConnection - }) + $currentTask = "Getting Resource Properties API-version: '$apiVersionToUse' ($apiRef); ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($resource.id)?api-version=$apiVersionToUse" + $method = 'GET' + $resourceResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -listenOn Content -unhandledErrorAction Continue + + if ($resourceResult -ne 'ResourceOrResourcegroupNotFound' -and $resourceResult -ne 'convertfromJSONError') { + $null = $script:arrayResourcesWithProperties.Add($resourceResult) + #$script:htResourcesWithProperties.($resourceResult.id) = $resourceResult + if ($resourceResult.properties.privateEndpointConnections.Count -gt 0) { + foreach ($privateEndpointConnection in $resourceResult.properties.privateEndpointConnections) { + $resourceResultIdSplit = $resourceResult.id -split '/' + $null = $script:arrayPrivateEndPointsFromResourceProperties.Add([PSCustomObject]@{ + ResourceName = $resourceResult.name + ResourceType = $resourceResult.type + ResourceId = $resourceResult.id + ResourceResourceGroup = $resourceResultIdSplit[4] + ResourceSubscriptionId = $scopeId + ResourceSubscriptionName = $scopeDisplayName + ResourceMGPath = $ChildMgParentNameChainDelimited + privateEndpointConnection = $privateEndpointConnection + }) + } + } + } + else { + if ($resourceResult -eq 'convertfromJSONError') { + $script:htResourcePropertiesConvertfromJSONFailed.($resource.id) = @{} } } } else { - if ($resourceResult -eq 'convertfromJSONError') { - $script:htResourcePropertiesConvertfromJSONFailed.($resource.id) = @{} - } + Write-Host "[Azure Governance Visualizer] Please file an issue at the Azure Governance Visualizer GitHub repository (aka.ms/AzGovViz) and provide this information (scrub subscription Id and company identifyable names): No API-version matches! ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)' - Thank you!" -ForegroundColor DarkRed } } else { - Write-Host "[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 + #Write-Host "$($resource.type) not in `$htAvailablePrivateEndpointTypes" } } - else { - #Write-Host "$($resource.type) not in `$htAvailablePrivateEndpointTypes" - } } -ThrottleLimit $azAPICallConf['htParameters'].ThrottleLimit } - #Write-Host 'arm resGet count:' $arrayResourcesWithProperties.Count #endregion resources GET - # if ($resourcesSubscriptionResult.Count -ne $arrayResourcesWithProperties.Count) { - # Write-Host " FYI: Getting Resources for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId'] - ARM list count: $($resourcesSubscriptionResult.Count); ARG get count: $($arrayResourcesWithProperties.Count)" - # } - #region PSRule if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { if ($resourcesSubscriptionResult.Count -gt 0) { @@ -1253,7 +1254,6 @@ function dataCollectionResources { foreach ($naming in $namingConvention) { if (($resource.name).StartsWith($naming, 'CurrentCultureIgnoreCase')) { $cafResourceNamingCheck = 'passed' - #$applicableNaming = $naming } } } @@ -1358,7 +1358,6 @@ function dataCollectionResourceGroups { $subscriptionQuotaId ) - #https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups?api-version=2020-06-01 $currentTask = "Getting ResourceGroups for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resourcegroups?api-version=2021-04-01" $method = 'GET' @@ -2193,12 +2192,11 @@ function dataCollectionPolicyDefinitions { if (-not [string]::IsNullOrEmpty($roledefinitionId)) { if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$hlpPolicyDefinitionId + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [System.Collections.ArrayList]@() + $null = $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($hlpPolicyDefinitionId) } else { - $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies - $usedInPolicies += $hlpPolicyDefinitionId - $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies.Add($hlpPolicyDefinitionId) } } else { @@ -2776,7 +2774,6 @@ function dataCollectionPolicyAssignmentsMG { $policySetCategory = $policySetDefinition.Category } else { - #test #Write-Host "pa '($L0mgmtGroupPolicyAssignment.Id)' scope: '$($scopeId)' - policySetDefinition not available: $policySetDefinitionId" Start-Sleep -Seconds 1 } @@ -3467,11 +3464,11 @@ function dataCollectionRoleDefinitions { if ($TargetMgOrSub -eq 'Sub') { $currentTask = "Getting Custom Role definitions for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-07-01&`$filter=type eq 'CustomRole'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'CustomRole'" } if ($TargetMgOrSub -eq 'MG') { $currentTask = "Getting Custom Role definitions for Management Group: '$($scopeDisplayName)' ('$scopeId')" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-07-01&`$filter=type eq 'CustomRole'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'CustomRole'" } $method = 'GET' $scopeCustomRoleDefinitions = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' @@ -3557,7 +3554,6 @@ function dataCollectionRoleAssignmentsMG { $roleAssignmentScheduleInstances = ($roleAssignmentScheduleInstancesFromAPI.where( { ($_.properties.roleAssignmentScheduleId -replace '.*/') -ne ($_.properties.originRoleAssignmentId -replace '.*/') })) $roleAssignmentScheduleInstancesCount = $roleAssignmentScheduleInstances.Count if ($roleAssignmentScheduleInstancesCount -gt 0) { - #$htRoleAssignmentsPIM = @{} foreach ($roleAssignmentScheduleInstance in $roleAssignmentScheduleInstances) { $script:htRoleAssignmentsPIM.($roleAssignmentScheduleInstance.properties.originRoleAssignmentId.tolower()) = $roleAssignmentScheduleInstance.properties } @@ -3839,7 +3835,6 @@ function dataCollectionRoleAssignmentsSub { $roleAssignmentScheduleInstances = ($roleAssignmentScheduleInstancesFromAPI.where( { ($_.properties.roleAssignmentScheduleId -replace '.*/') -ne ($_.properties.originRoleAssignmentId -replace '.*/') })) $roleAssignmentScheduleInstancesCount = $roleAssignmentScheduleInstances.Count if ($roleAssignmentScheduleInstancesCount -gt 0) { - #$htRoleAssignmentsPIM = @{} foreach ($roleAssignmentScheduleInstance in $roleAssignmentScheduleInstances) { $script:htRoleAssignmentsPIM.($roleAssignmentScheduleInstance.properties.originRoleAssignmentId.tolower()) = $roleAssignmentScheduleInstance.properties } @@ -4193,4 +4188,4 @@ function dataCollectionClassicAdministratorsSub { } $funcDataCollectionClassicAdministratorsSub = $function:dataCollectionClassicAdministratorsSub.ToString() -#endregion functions4DataCollection +#endregion functions4DataCollection \ No newline at end of file diff --git a/pwsh/dev/functions/detectPolicyEffect.ps1 b/pwsh/dev/functions/detectPolicyEffect.ps1 index 2384226a..14fa0ad5 100644 --- a/pwsh/dev/functions/detectPolicyEffect.ps1 +++ b/pwsh/dev/functions/detectPolicyEffect.ps1 @@ -47,10 +47,10 @@ function detectPolicyEffect { 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 = @() + $arrayAllowed = [System.Collections.ArrayList]@() foreach ($allowedValue in $policyDefinition.properties.parameters.($Match.Value).allowedValues) { if ($allowedValue -in $ValidPolicyEffects) { - $arrayAllowed += $allowedValue + $null = $arrayAllowed.Add($allowedValue) } else { Write-Host "invalid allowedValue effect $($allowedValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" diff --git a/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 b/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 index c903f3fc..3942e623 100644 --- a/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 +++ b/pwsh/dev/functions/getMDfCSecureScoreMG.ps1 @@ -33,25 +33,22 @@ function getMDfCSecureScoreMG { } "@ - $getMgAscSecureScore = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' - + $getMgAscSecureScore = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' -unhandledErrorAction ContinueQuiet if ($getMgAscSecureScore) { - if ($getMgAscSecureScore -eq 'capitulation') { - Write-Host ' Microsoft Defender for Cloud SecureScore for Management Groups will not be available' -ForegroundColor Yellow - } - else { - Write-Host " Retrieved 'Microsoft Defender for Cloud' SecureScore for $($getMgAscSecureScore.Count) Management Groups" - foreach ($entry in $getMgAscSecureScore) { - $script:htMgASCSecureScore.($entry.mgId) = @{} - if ($entry.secureScore -eq 404) { - $script:htMgASCSecureScore.($entry.mgId).SecureScore = 'n/a' - } - else { - $script:htMgASCSecureScore.($entry.mgId).SecureScore = $entry.secureScore - } + Write-Host " Retrieved 'Microsoft Defender for Cloud' SecureScore for $($getMgAscSecureScore.Count) Management Groups" + foreach ($entry in $getMgAscSecureScore) { + $script:htMgASCSecureScore.($entry.mgId) = @{} + if ($entry.secureScore -eq 404) { + $script:htMgASCSecureScore.($entry.mgId).SecureScore = 'n/a' + } + else { + $script:htMgASCSecureScore.($entry.mgId).SecureScore = $entry.secureScore } } } + else { + Write-Host ' Microsoft Defender for Cloud SecureScore for Management Groups will not be available' -ForegroundColor Yellow + } $end = Get-Date Write-Host "Getting Microsoft Defender for Cloud Secure Score for Management Groups duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" diff --git a/pwsh/dev/functions/getPIMEligible.ps1 b/pwsh/dev/functions/getPIMEligible.ps1 index 16cfb5e8..d97185bb 100644 --- a/pwsh/dev/functions/getPIMEligible.ps1 +++ b/pwsh/dev/functions/getPIMEligible.ps1 @@ -53,143 +53,156 @@ function getPIMEligible { $htPIMEligibleDirect = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} $relevantSubscriptionIds = $subsToProcessInCustomDataCollection.subscriptionId - $scopesToIterate | ForEach-Object -Parallel { - $scope = $_ - $azAPICallConf = $using:azAPICallConf - $arrayPIMEligible = $using:arrayPIMEligible - $htPIMEligibleDirect = $using:htPIMEligibleDirect - if ($scope.type -eq 'managementgroup') { $htManagementGroupsMgPath = $using:htManagementGroupsMgPath } - if ($scope.type -eq 'subscription') { $htSubscriptionsMgPath = $using:htSubscriptionsMgPath } - $htPrincipals = $using:htPrincipals - $htUserTypesGuest = $using:htUserTypesGuest - $htServicePrincipals = $using:htServicePrincipals - $relevantSubscriptionIds = $using:relevantSubscriptionIds - $function:resolveObjectIds = $using:funcResolveObjectIds - $function:testGuid = $using:funcTestGuid - - $processThisScope = $true - if ($scope.type -eq 'subscription') { - if (($scope.externalId -replace '.*/') -notin $relevantSubscriptionIds) { - Write-Host " Non relevant subscriptionId '$(($scope.externalId -replace '.*/'))' /skipping this subscription as it is not contained in the 'Relevant Subscriptions' collection (needs investigation)" -ForegroundColor DarkRed - $processThisScope = $false - } - } - if ($processThisScope -eq $true) { - $currentTask = "Get Eligible assignments for Scope $($scope.type): $($scope.externalId -replace '.*/')" - $extUri = "?`$expand=linkedEligibleRoleAssignment,subject,roleDefinition(`$expand=resource)&`$count=true&`$filter=(roleDefinition/resource/id eq '$($scope.id)')+and+(assignmentState eq 'Eligible')&`$top=100" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/roleAssignments" + $extUri - $resx = AzAPICall -AzAPICallConfiguration $azapicallConf -currentTask $currentTask -uri $uri + if ($scopesToIterate.Count -gt 0) { + + $batchSize = [math]::ceiling($scopesToIterate.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $scopesToIterateBatch = ($scopesToIterate) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($scopesToIterateBatch.Count) batches" + + $scopesToIterateBatch | ForEach-Object -Parallel { + $scope = $_ + $azAPICallConf = $using:azAPICallConf + $arrayPIMEligible = $using:arrayPIMEligible + $htPIMEligibleDirect = $using:htPIMEligibleDirect + $htPrincipals = $using:htPrincipals + $htUserTypesGuest = $using:htUserTypesGuest + $htServicePrincipals = $using:htServicePrincipals + $relevantSubscriptionIds = $using:relevantSubscriptionIds + $function:resolveObjectIds = $using:funcResolveObjectIds + $function:testGuid = $using:funcTestGuid + + foreach ($scope in $_.Group) { + if ($scope.type -eq 'managementgroup') { $htManagementGroupsMgPath = $using:htManagementGroupsMgPath } + if ($scope.type -eq 'subscription') { $htSubscriptionsMgPath = $using:htSubscriptionsMgPath } + + $processThisScope = $true + if ($scope.type -eq 'subscription') { + if (($scope.externalId -replace '.*/') -notin $relevantSubscriptionIds) { + Write-Host " Non relevant subscriptionId '$(($scope.externalId -replace '.*/'))' /skipping this subscription as it is not contained in the 'Relevant Subscriptions' collection (needs investigation)" -ForegroundColor DarkRed + $processThisScope = $false + } + } - if ($resx.Count -gt 0) { + if ($processThisScope -eq $true) { + $currentTask = "Get Eligible assignments for Scope $($scope.type): $($scope.externalId -replace '.*/')" + $extUri = "?`$expand=linkedEligibleRoleAssignment,subject,roleDefinition(`$expand=resource)&`$count=true&`$filter=(roleDefinition/resource/id eq '$($scope.id)')+and+(assignmentState eq 'Eligible')&`$top=100" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/roleAssignments" + $extUri + $resx = AzAPICall -AzAPICallConfiguration $azapicallConf -currentTask $currentTask -uri $uri - $users = $resx.where({ $_.subject.type -eq 'user' }) - if ($users.Count -gt 0) { - ResolveObjectIds -objectIds $users.subject.id -showActivity - } + if ($resx.Count -gt 0) { - foreach ($entry in $resx) { - $scopeId = $scope.externalId -replace '.*/' - if ($scope.type -eq 'managementgroup') { - $ScopeType = 'MG' - $ManagementGroupId = $scopeId - $SubscriptionId = '' - $SubscriptionDisplayName = '' - if ($htManagementGroupsMgPath.($scopeId)) { - $MgDetails = $htManagementGroupsMgPath.($scopeId) - $ManagementGroupDisplayName = $MgDetails.DisplayName - $ScopeDisplayName = $MgDetails.DisplayName - $MgPath = $MgDetails.path - $MgLevel = $MgDetails.level - } - else { - $ManagementGroupDisplayName = 'notAccessible' - $ScopeDisplayName = 'notAccessible' - $MgPath = 'notAccessible' - $MgLevel = 'notAccessible' + $users = $resx.where({ $_.subject.type -eq 'user' }) + if ($users.Count -gt 0) { + ResolveObjectIds -objectIds $users.subject.id -showActivity } - if ($entry.memberType -eq 'direct') { - $script:htPIMEligibleDirect.($entry.id) = @{} - $script:htPIMEligibleDirect.($entry.id).clear = $scopeId - if ($scopeId -eq $ManagementGroupDisplayName) { - $script:htPIMEligibleDirect.($entry.id).enriched = "$($scopeId) [Level $($MgLevel)]" + foreach ($entry in $resx) { + $scopeId = $scope.externalId -replace '.*/' + if ($scope.type -eq 'managementgroup') { + $ScopeType = 'MG' + $ManagementGroupId = $scopeId + $SubscriptionId = '' + $SubscriptionDisplayName = '' + if ($htManagementGroupsMgPath.($scopeId)) { + $MgDetails = $htManagementGroupsMgPath.($scopeId) + $ManagementGroupDisplayName = $MgDetails.DisplayName + $ScopeDisplayName = $MgDetails.DisplayName + $MgPath = $MgDetails.path + $MgLevel = $MgDetails.level + } + else { + $ManagementGroupDisplayName = 'notAccessible' + $ScopeDisplayName = 'notAccessible' + $MgPath = 'notAccessible' + $MgLevel = 'notAccessible' + } + + if ($entry.memberType -eq 'direct') { + $script:htPIMEligibleDirect.($entry.id) = @{} + $script:htPIMEligibleDirect.($entry.id).clear = $scopeId + if ($scopeId -eq $ManagementGroupDisplayName) { + $script:htPIMEligibleDirect.($entry.id).enriched = "$($scopeId) [Level $($MgLevel)]" + } + else { + $script:htPIMEligibleDirect.($entry.id).enriched = "$($ManagementGroupDisplayName) ($($scopeId)) [Level $($MgLevel)]" + } + } } - else { - $script:htPIMEligibleDirect.($entry.id).enriched = "$($ManagementGroupDisplayName) ($($scopeId)) [Level $($MgLevel)]" + if ($scope.type -eq 'subscription') { + $ScopeType = 'Sub' + #$ManagementGroupId = '' + $SubscriptionId = $scopeId + if ($htSubscriptionsMgPath.($scopeId)) { + $MgDetails = $htSubscriptionsMgPath.($scopeId) + $SubscriptionDisplayName = $MgDetails.DisplayName + $ScopeDisplayName = $MgDetails.DisplayName + $MgPath = $MgDetails.path + $MgLevel = $MgDetails.level + $ManagementGroupId = $MgDetails.Parent + $ManagementGroupDisplayName = $MgDetails.ParentName + } + else { + $SubscriptionDisplayName = 'notAccessible' + $ScopeDisplayName = 'notAccessible' + $MgPath = 'notAccessible' + $MgLevel = 'notAccessible' + } + #$ManagementGroupDisplayName = '' + } - } - } - if ($scope.type -eq 'subscription') { - $ScopeType = 'Sub' - #$ManagementGroupId = '' - $SubscriptionId = $scopeId - if ($htSubscriptionsMgPath.($scopeId)) { - $MgDetails = $htSubscriptionsMgPath.($scopeId) - $SubscriptionDisplayName = $MgDetails.DisplayName - $ScopeDisplayName = $MgDetails.DisplayName - $MgPath = $MgDetails.path - $MgLevel = $MgDetails.level - $ManagementGroupId = $MgDetails.Parent - $ManagementGroupDisplayName = $MgDetails.ParentName - } - else { - $SubscriptionDisplayName = 'notAccessible' - $ScopeDisplayName = 'notAccessible' - $MgPath = 'notAccessible' - $MgLevel = 'notAccessible' - } - #$ManagementGroupDisplayName = '' - } + if ($entry.subject.type -eq 'user') { + if ($htPrincipals.($entry.subject.id)) { + $userDetail = $htPrincipals.($entry.subject.id) + $principalType = "$($userDetail.type) $($userDetail.userType)" + } + else { + $principalType = $entry.subject.type + } + } + else { + $principalType = $entry.subject.type + } - if ($entry.subject.type -eq 'user') { - if ($htPrincipals.($entry.subject.id)) { - $userDetail = $htPrincipals.($entry.subject.id) - $principalType = "$($userDetail.type) $($userDetail.userType)" + $roleType = 'undefined' + if ($entry.roleDefinition.type -eq 'BuiltInRole') { $roleType = 'Builtin' } + if ($entry.roleDefinition.type -eq 'CustomRole') { $roleType = 'Custom' } + + $null = $script:arrayPIMEligible.Add([PSCustomObject]@{ + ScopeType = $ScopeType + ScopeId = $scopeId + ScopeDisplayName = $ScopeDisplayName + ManagementGroupId = $ManagementGroupId + ManagementGroupDisplayName = $ManagementGroupDisplayName + SubscriptionId = $SubscriptionId + SubscriptionDisplayName = $SubscriptionDisplayName + MgPath = $MgPath + MgLevel = $MgLevel + RoleId = $entry.roleDefinition.externalId + RoleIdGuid = $entry.roleDefinition.externalId -replace '.*/' + RoleType = $roleType + RoleName = $entry.roleDefinition.displayName + IdentityObjectId = $entry.subject.id + IdentityType = $principalType + IdentityDisplayName = $entry.subject.displayName + IdentityPrincipalName = $entry.subject.principalName + PIMId = $entry.id + PIMInheritance = $entry.memberType + PIMInheritedFromClear = '' + PIMInheritedFrom = '' + PIMStartDateTime = $entry.startDateTime + PIMEndDateTime = $entry.endDateTime + }) } - else { - $principalType = $entry.subject.type - } - } - else { - $principalType = $entry.subject.type } - - $roleType = 'undefined' - if ($entry.roleDefinition.type -eq 'BuiltInRole') { $roleType = 'Builtin' } - if ($entry.roleDefinition.type -eq 'CustomRole') { $roleType = 'Custom' } - - $null = $script:arrayPIMEligible.Add([PSCustomObject]@{ - ScopeType = $ScopeType - ScopeId = $scopeId - ScopeDisplayName = $ScopeDisplayName - ManagementGroupId = $ManagementGroupId - ManagementGroupDisplayName = $ManagementGroupDisplayName - SubscriptionId = $SubscriptionId - SubscriptionDisplayName = $SubscriptionDisplayName - MgPath = $MgPath - MgLevel = $MgLevel - RoleId = $entry.roleDefinition.externalId - RoleIdGuid = $entry.roleDefinition.externalId -replace '.*/' - RoleType = $roleType - RoleName = $entry.roleDefinition.displayName - IdentityObjectId = $entry.subject.id - IdentityType = $principalType - IdentityDisplayName = $entry.subject.displayName - IdentityPrincipalName = $entry.subject.principalName - PIMId = $entry.id - PIMInheritance = $entry.memberType - PIMInheritedFromClear = '' - PIMInheritedFrom = '' - PIMStartDateTime = $entry.startDateTime - PIMEndDateTime = $entry.endDateTime - }) } } - } - } -ThrottleLimit $ThrottleLimit + } -ThrottleLimit $ThrottleLimit + } foreach ($entry in $arrayPIMEligible) { if ($entry.PIMInheritance -eq 'inherited') { diff --git a/pwsh/dev/functions/getResourceDiagnosticsCapability.ps1 b/pwsh/dev/functions/getResourceDiagnosticsCapability.ps1 index c25b3d8b..727ffc0b 100644 --- a/pwsh/dev/functions/getResourceDiagnosticsCapability.ps1 +++ b/pwsh/dev/functions/getResourceDiagnosticsCapability.ps1 @@ -42,7 +42,7 @@ function getResourceDiagnosticsCapability { #thx @Jim Britt (Microsoft) https://github.com/JimGBritt/AzurePolicy/tree/master/AzureMonitor/Scripts Create-AzDiagPolicy.ps1 $responseJSON = '' - $logCategories = @() + $logCategories = [System.Collections.ArrayList]@() $metrics = $false $logs = $false @@ -102,7 +102,7 @@ function getResourceDiagnosticsCapability { } if ($response.properties.categoryType -eq 'Logs') { $logs = $true - $logCategories += $response.name + $null = $logCategories.Add($response.name) } } } diff --git a/pwsh/dev/functions/html/htmlFunctions.ps1 b/pwsh/dev/functions/html/htmlFunctions.ps1 index a30810c8..b8fef36c 100644 --- a/pwsh/dev/functions/html/htmlFunctions.ps1 +++ b/pwsh/dev/functions/html/htmlFunctions.ps1 @@ -238,7 +238,7 @@ function processScopeInsights($mgChild, $mgChildOf) { "@ if ($mgId -eq $defaultManagementGroupId) { $script:html += @' - + '@ } $script:html += @" diff --git a/pwsh/dev/functions/namingValidation.ps1 b/pwsh/dev/functions/namingValidation.ps1 index 752b1da4..7c39c788 100644 --- a/pwsh/dev/functions/namingValidation.ps1 +++ b/pwsh/dev/functions/namingValidation.ps1 @@ -1,16 +1,16 @@ function NamingValidation($toCheck) { $checks = @(':', '/', '\', '<', '>', '|', '"') - $array = @() + $array = [System.Collections.ArrayList]@() foreach ($check in $checks) { if ($toCheck -like "*$($check)*") { - $array += $check + $null = $array.Add($check) } } if ($toCheck -match '\*') { - $array += '*' + $null = $array.Add('*') } if ($toCheck -match '\?') { - $array += '?' + $null = $array.Add('?') } return $array } \ No newline at end of file diff --git a/pwsh/dev/functions/processAADGroups.ps1 b/pwsh/dev/functions/processAADGroups.ps1 index 587d7462..c3d209ae 100644 --- a/pwsh/dev/functions/processAADGroups.ps1 +++ b/pwsh/dev/functions/processAADGroups.ps1 @@ -54,8 +54,15 @@ function processAADGroups { Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" - $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { - $aadGroupIdWithRoleAssignment = $_ + $ThrottleLimitThis = $ThrottleLimit * 2 + $batchSize = [math]::ceiling($optimizedTableForAADGroupsQuery.Count / $ThrottleLimitThis) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $optimizedTableForAADGroupsQueryBatch = ($optimizedTableForAADGroupsQuery) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($optimizedTableForAADGroupsQueryBatch.Count) batches" + + $optimizedTableForAADGroupsQueryBatch | ForEach-Object -Parallel { + #$aadGroupIdWithRoleAssignment = $_ #region UsingVARs #fromOtherFunctions $AADGroupMembersLimit = $using:AADGroupMembersLimit @@ -74,41 +81,41 @@ function processAADGroups { $function:getGroupmembers = $using:funcGetGroupmembers #endregion UsingVARs - $rndom = Get-Random -Minimum 10 -Maximum 750 - Start-Sleep -Millisecond $rndom + foreach ($aadGroupIdWithRoleAssignment in $_.Group) { - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)/transitiveMembers/`$count" - $method = 'GET' - $aadGroupMembersCount = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupMembersCountTransitive $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)" -listenOn 'Content' -consistencyLevel 'eventual' + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)/transitiveMembers/`$count" + $method = 'GET' + $aadGroupMembersCount = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupMembersCountTransitive $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)" -listenOn 'Content' -consistencyLevel 'eventual' - if ($aadGroupMembersCount -eq 'Request_ResourceNotFound') { - $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ - groupId = $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId - }) - } - else { - if ($aadGroupMembersCount -gt $AADGroupMembersLimit) { - Write-Host " Group exceeding limit ($($AADGroupMembersLimit)); memberCount: $aadGroupMembersCount; Group: $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname) ($($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)); Members will not be resolved adjust the limit using parameter -AADGroupMembersLimit" - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) = @{} - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersAllCount = $aadGroupMembersCount - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersUsersCount = 'n/a' - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersGroupsCount = 'n/a' - $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersServicePrincipalsCount = 'n/a' + if ($aadGroupMembersCount -eq 'Request_ResourceNotFound') { + $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ + groupId = $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId + }) } else { - getGroupmembers -aadGroupId $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId -aadGroupDisplayName $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname + if ($aadGroupMembersCount -gt $AADGroupMembersLimit) { + Write-Host " Group exceeding limit ($($AADGroupMembersLimit)); memberCount: $aadGroupMembersCount; Group: $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname) ($($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)); Members will not be resolved adjust the limit using parameter -AADGroupMembersLimit" + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) = @{} + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersAllCount = $aadGroupMembersCount + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersUsersCount = 'n/a' + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersGroupsCount = 'n/a' + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersServicePrincipalsCount = 'n/a' + } + else { + getGroupmembers -aadGroupId $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId -aadGroupDisplayName $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname + } } - } - $null = $script:arrayProgressedAADGroups.Add($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) - $processedAADGroupsCount = $null - $processedAADGroupsCount = ($arrayProgressedAADGroups).Count - if ($processedAADGroupsCount) { - if ($processedAADGroupsCount % $indicator -eq 0) { - Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" + $null = $script:arrayProgressedAADGroups.Add($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) + $processedAADGroupsCount = $null + $processedAADGroupsCount = ($arrayProgressedAADGroups).Count + if ($processedAADGroupsCount) { + if ($processedAADGroupsCount % $indicator -eq 0) { + Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" + } } } - } -ThrottleLimit ($ThrottleLimit * 2) + } -ThrottleLimit ($ThrottleLimitThis) } else { Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" diff --git a/pwsh/dev/functions/processApplications.ps1 b/pwsh/dev/functions/processApplications.ps1 index 015ff84d..dcad6650 100644 --- a/pwsh/dev/functions/processApplications.ps1 +++ b/pwsh/dev/functions/processApplications.ps1 @@ -17,7 +17,15 @@ function processApplications { $startSPApp = Get-Date $currentDateUTC = (Get-Date).ToUniversalTime() $script:arrayApplicationRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) - $servicePrincipalsOfTypeApplication | ForEach-Object -Parallel { + + $ThrottleLimitThis = $ThrottleLimit * 2 + $batchSize = [math]::ceiling($servicePrincipalsOfTypeApplication.Count / $ThrottleLimitThis) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $servicePrincipalsOfTypeApplicationBatch = ($servicePrincipalsOfTypeApplication) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($servicePrincipalsOfTypeApplicationBatch.Count) batches" + + $servicePrincipalsOfTypeApplicationBatch | ForEach-Object -Parallel { #region UsingVARs $currentDateUTC = $using:currentDateUTC @@ -30,91 +38,92 @@ function processApplications { $htServicePrincipals = $using:htServicePrincipals #endregion UsingVARs - $sp = $htServicePrincipals.($_) + foreach ($entry in $_.Group) { + $sp = $htServicePrincipals.($entry) - $currentTask = "getApp $($sp.appId)" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" - $method = 'GET' - $getApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + $currentTask = "getApp $($sp.appId)" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" + $method = 'GET' + $getApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask - if ($getApplication -eq 'Request_ResourceNotFound') { - $null = $script:arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ - appId = $sp.appId - }) - } - else { - if (($getApplication).Count -eq 0) { - Write-Host "$($sp.appId) no data returned / seems non existent?" + if ($getApplication -eq 'Request_ResourceNotFound') { + $null = $script:arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ + appId = $sp.appId + }) } else { - $script:htAppDetails.($sp.id) = @{} - $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType - $script:htAppDetails.($sp.id).spGraphDetails = $sp - $script:htAppDetails.($sp.id).appGraphDetails = $getApplication + if (($getApplication).Count -eq 0) { + Write-Host "$($sp.appId) no data returned / seems non existent?" + } + else { + $script:htAppDetails.($sp.id) = @{} + $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType + $script:htAppDetails.($sp.id).spGraphDetails = $sp + $script:htAppDetails.($sp.id).appGraphDetails = $getApplication - $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count - if ($appPasswordCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount - $appPasswordCredentialsExpiredCount = 0 - $appPasswordCredentialsGracePeriodExpiryCount = 0 - $appPasswordCredentialsExpiryOKCount = 0 - $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appPasswordCredential in $getApplication.passwordCredentials) { - $passwordExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays - if ($passwordExpiryTotalDays -lt 0) { - $appPasswordCredentialsExpiredCount++ - } - elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appPasswordCredentialsGracePeriodExpiryCount++ - } - else { - if ($passwordExpiryTotalDays -gt 730) { - $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ + $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count + if ($appPasswordCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount + $appPasswordCredentialsExpiredCount = 0 + $appPasswordCredentialsGracePeriodExpiryCount = 0 + $appPasswordCredentialsExpiryOKCount = 0 + $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appPasswordCredential in $getApplication.passwordCredentials) { + $passwordExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays + if ($passwordExpiryTotalDays -lt 0) { + $appPasswordCredentialsExpiredCount++ + } + elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appPasswordCredentialsGracePeriodExpiryCount++ } else { - $appPasswordCredentialsExpiryOKCount++ + if ($passwordExpiryTotalDays -gt 730) { + $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appPasswordCredentialsExpiryOKCount++ + } } } + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount + $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount } - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount - $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount - } - $appKeyCredentialsCount = ($getApplication.keyCredentials).count - if ($appKeyCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount - $appKeyCredentialsExpiredCount = 0 - $appKeyCredentialsGracePeriodExpiryCount = 0 - $appKeyCredentialsExpiryOKCount = 0 - $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appKeyCredential in $getApplication.keyCredentials) { - $keyCredentialExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays - if ($keyCredentialExpiryTotalDays -lt 0) { - $appKeyCredentialsExpiredCount++ - } - elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appKeyCredentialsGracePeriodExpiryCount++ - } - else { - if ($keyCredentialExpiryTotalDays -gt 730) { - $appKeyCredentialsExpiryOKMoreThan2YearsCount++ + $appKeyCredentialsCount = ($getApplication.keyCredentials).count + if ($appKeyCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount + $appKeyCredentialsExpiredCount = 0 + $appKeyCredentialsGracePeriodExpiryCount = 0 + $appKeyCredentialsExpiryOKCount = 0 + $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appKeyCredential in $getApplication.keyCredentials) { + $keyCredentialExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays + if ($keyCredentialExpiryTotalDays -lt 0) { + $appKeyCredentialsExpiredCount++ + } + elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appKeyCredentialsGracePeriodExpiryCount++ } else { - $appKeyCredentialsExpiryOKCount++ + if ($keyCredentialExpiryTotalDays -gt 730) { + $appKeyCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appKeyCredentialsExpiryOKCount++ + } } } + $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount + $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount } - $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount - $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount } } } - - } -ThrottleLimit ($ThrottleLimit * 2) + } -ThrottleLimit ($ThrottleLimitThis) $endSPApp = Get-Date Write-Host "Processing Service Principals - Applications duration: $((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalSeconds) seconds)" diff --git a/pwsh/dev/functions/processDataCollection.ps1 b/pwsh/dev/functions/processDataCollection.ps1 index 73888e26..6ac86285 100644 --- a/pwsh/dev/functions/processDataCollection.ps1 +++ b/pwsh/dev/functions/processDataCollection.ps1 @@ -15,13 +15,13 @@ function processDataCollection { $btchCnt = 0 foreach ($btch in $mgBatch) { $btchCnt++ - $listOfMGs = @() + $listOfMGs = [System.Collections.ArrayList]@() foreach ($btchMg in $btch.Group | Sort-Object -Property name) { if ($btchMg.name -eq $btchMg.Properties.displayName) { - $listOfMGs += $btchMg.name + $null = $listOfMGs.Add($btchMg.name) } else { - $listOfMGs += "$($btchMg.name) ($($btchMg.Properties.displayName))" + $null = $listOfMGs.Add("$($btchMg.name) ($($btchMg.Properties.displayName))") } } Write-Host " Batch#$($btchCnt) - $($listOfMGs.Count) Management Groups: $($listOfMGs -join ', ')" @@ -32,8 +32,13 @@ function processDataCollection { showMemoryUsage - $batchLevel.Group | ForEach-Object -Parallel { - $mgdetail = $_ + $batchSize = [math]::ceiling($batchLevel.Group.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchLevelGroupBatch = ($batchLevel.Group) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($batchLevelGroupBatch.Count) batches" + + $batchLevelGroupBatch | ForEach-Object -Parallel { #region UsingVARs #Parameters MG&Sub related $CsvDelimiter = $using:CsvDelimiter @@ -108,103 +113,115 @@ function processDataCollection { #endregion usingVARS $builtInPolicyDefinitionsCount = $using:builtInPolicyDefinitionsCount - $addRowToTableDone = $false + foreach ($mgdetail in $_.Group) { - $MgDetailThis = $htManagementGroupsMgPath.($mgdetail.Name) - $MgParentId = $MgDetailThis.Parent - $hierarchyLevel = $MgDetailThis.ParentNameChainCount + $addRowToTableDone = $false - if ($MgParentId -eq '__TenantRoot__') { - $MgParentId = 'TenantRoot' - $MgParentName = $MgParentId - } - else { - $MgParentName = $htManagementGroupsMgPath.($MgParentId).DisplayName - } + $MgDetailThis = $htManagementGroupsMgPath.($mgdetail.Name) + $MgParentId = $MgDetailThis.Parent + $hierarchyLevel = $MgDetailThis.ParentNameChainCount + + if ($MgParentId -eq '__TenantRoot__') { + $MgParentId = 'TenantRoot' + $MgParentName = $MgParentId + } + else { + $MgParentName = $htManagementGroupsMgPath.($MgParentId).DisplayName + } - $rndom = Get-Random -Minimum 10 -Maximum 750 - Start-Sleep -Millisecond $rndom - $startMgLoopThis = Get-Date + $rndom = Get-Random -Minimum 10 -Maximum 750 + Start-Sleep -Millisecond $rndom + $startMgLoopThis = Get-Date - if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { + if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { - #namingValidation - if (-not [string]::IsNullOrEmpty($mgdetail.properties.displayName)) { - $namingValidationResult = NamingValidation -toCheck $mgdetail.properties.displayName - if ($namingValidationResult.Count -gt 0) { - $script:htNamingValidation.ManagementGroup.($mgdetail.Name) = @{} - $script:htNamingValidation.ManagementGroup.($mgdetail.Name).nameInvalidChars = ($namingValidationResult -join '') - $script:htNamingValidation.ManagementGroup.($mgdetail.Name).name = $mgdetail.properties.displayName + #namingValidation + if (-not [string]::IsNullOrEmpty($mgdetail.properties.displayName)) { + $namingValidationResult = NamingValidation -toCheck $mgdetail.properties.displayName + if ($namingValidationResult.Count -gt 0) { + $script:htNamingValidation.ManagementGroup.($mgdetail.Name) = @{} + $script:htNamingValidation.ManagementGroup.($mgdetail.Name).nameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.ManagementGroup.($mgdetail.Name).name = $mgdetail.properties.displayName + } } - } - $targetMgOrSub = 'MG' - $baseParameters = @{ - scopeId = $mgdetail.Name - scopeDisplayName = $mgdetail.properties.displayName - } + $targetMgOrSub = 'MG' + $baseParameters = @{ + scopeId = $mgdetail.Name + scopeDisplayName = $mgdetail.properties.displayName + } - #ManagementGroupASCSecureScore - $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $mgdetail.Name + #ManagementGroupASCSecureScore + $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $mgdetail.Name - $addRowToTableParameters = @{ - hierarchyLevel = $hierarchyLevel - mgParentId = $mgParentId - mgParentName = $mgParentName - mgAscSecureScoreResult = $mgAscSecureScoreResult - } + $addRowToTableParameters = @{ + hierarchyLevel = $hierarchyLevel + mgParentId = $mgParentId + mgParentName = $mgParentName + mgAscSecureScoreResult = $mgAscSecureScoreResult + } - #mg diag - DataCollectionDiagnosticsMG @baseParameters + #mg diag + DataCollectionDiagnosticsMG @baseParameters - if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { - #MGPolicyCompliance - DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub - } + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + #MGPolicyCompliance + DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub + } - #MGBlueprintDefinitions - $functionReturn = DataCollectionBluePrintDefinitionsMG @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } + #MGBlueprintDefinitions + $functionReturn = DataCollectionBluePrintDefinitionsMG @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } - #MGPolicyExemptions - DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub + #MGPolicyExemptions + DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub - #MGPolicyDefinitions - $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' + #MGPolicyDefinitions + $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' - #MGPolicySetDefinitions - $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' + #MGPolicySetDefinitions + $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' - if (-not $htMgAtScopePoliciesScoped.($mgdetail.Name)) { - $script:htMgAtScopePoliciesScoped.($mgdetail.Name) = @{} - $script:htMgAtScopePoliciesScoped.($mgdetail.Name).ScopedCount = $policyDefinitionsScopedCount + $policySetDefinitionsScopedCount - } + if (-not $htMgAtScopePoliciesScoped.($mgdetail.Name)) { + $script:htMgAtScopePoliciesScoped.($mgdetail.Name) = @{} + $script:htMgAtScopePoliciesScoped.($mgdetail.Name).ScopedCount = $policyDefinitionsScopedCount + $policySetDefinitionsScopedCount + } - $scopedPolicyCounts = @{ - policyDefinitionsScopedCount = $policyDefinitionsScopedCount - policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount - } + $scopedPolicyCounts = @{ + policyDefinitionsScopedCount = $policyDefinitionsScopedCount + policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount + } - #MgPolicyAssignments - $functionReturn = DataCollectionPolicyAssignmentsMG @baseParameters @addRowToTableParameters @scopedPolicyCounts - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } + #MgPolicyAssignments + $functionReturn = DataCollectionPolicyAssignmentsMG @baseParameters @addRowToTableParameters @scopedPolicyCounts + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } - #MGRoleDefinitions - DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + #MGRoleDefinitions + DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub - #MGRoleAssignments - $functionReturn = DataCollectionRoleAssignmentsMG @baseParameters @addRowToTableParameters - if ($functionReturn.'addRowToTableDone') { - $addRowToTableDone = $true - } + #MGRoleAssignments + $functionReturn = DataCollectionRoleAssignmentsMG @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } - if ($addRowToTableDone -ne $true) { + if ($addRowToTableDone -ne $true) { + addRowToTable ` + -level $hierarchyLevel ` + -mgName $mgdetail.properties.displayName ` + -mgId $mgdetail.Name ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult + } + } + else { addRowToTable ` -level $hierarchyLevel ` -mgName $mgdetail.properties.displayName ` @@ -213,29 +230,19 @@ function processDataCollection { -mgParentName $mgParentName ` -mgASCSecureScore $mgAscSecureScoreResult } - } - else { - addRowToTable ` - -level $hierarchyLevel ` - -mgName $mgdetail.properties.displayName ` - -mgId $mgdetail.Name ` - -mgParentId $mgParentId ` - -mgParentName $mgParentName ` - -mgASCSecureScore $mgAscSecureScoreResult - } - - $endMgLoopThis = Get-Date - $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ - Type = 'Mg' - Id = $mgdetail.Name - DurationSec = (New-TimeSpan -Start $startMgLoopThis -End $endMgLoopThis).TotalSeconds - }) - $null = $script:arrayDataCollectionProgressMg.Add($mgdetail.Name) - $progressCount = ($arrayDataCollectionProgressMg).Count - Write-Host " $($progressCount)/$($allManagementGroupsFromEntitiesChildOfRequestedMgCount) Management Groups processed" + $endMgLoopThis = Get-Date + $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ + Type = 'Mg' + Id = $mgdetail.Name + DurationSec = (New-TimeSpan -Start $startMgLoopThis -End $endMgLoopThis).TotalSeconds + }) + $null = $script:arrayDataCollectionProgressMg.Add($mgdetail.Name) + $progressCount = ($arrayDataCollectionProgressMg).Count + Write-Host " $($progressCount)/$($allManagementGroupsFromEntitiesChildOfRequestedMgCount) Management Groups processed" + } } -ThrottleLimit $ThrottleLimit } @@ -270,133 +277,128 @@ function processDataCollection { $startSubLoop = Get-Date if ($subsToProcessInCustomDataCollectionCount -gt 0) { + $batchSize = [math]::ceiling($subsToProcessInCustomDataCollectionCount / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" $counterBatch = [PSCustomObject] @{ Value = 0 } - $batchSize = 100 - if ($subsToProcessInCustomDataCollectionCount -gt 500) { - $batchSize = 200 - } - Write-Host " Subscriptions Batch size: $batchSize" - - $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } - $batchCnt = 0 - foreach ($batch in $subscriptionsBatch) { - $startBatch = Get-Date - $batchCnt++ - Write-Host " processing Batch #$batchCnt/$(($subscriptionsBatch | Measure-Object).Count) ($(($batch.Group | Measure-Object).Count) Subscriptions)" - showMemoryUsage - - $batch.Group | ForEach-Object -Parallel { - $startSubLoopThis = Get-Date - $childMgSubDetail = $_ - #region UsingVARs - #Parameters MG&Sub related - $CsvDelimiter = $using:CsvDelimiter - $CsvDelimiterOpposite = $using:CsvDelimiterOpposite - #Parameters Sub related - #fromOtherFunctions - $azAPICallConf = $using:azAPICallConf - $scriptPath = $using:ScriptPath - #Array&HTs - $newTable = $using:newTable - $storageAccounts = $using:storageAccounts - $resourcesAll = $using:resourcesAll - $resourcesIdsAll = $using:resourcesIdsAll - $resourceGroupsAll = $using:resourceGroupsAll - $customDataCollectionDuration = $using:customDataCollectionDuration - $htSubscriptionsMgPath = $using:htSubscriptionsMgPath - $htManagementGroupsMgPath = $using:htManagementGroupsMgPath - $htResourceProvidersAll = $using:htResourceProvidersAll - $arrayFeaturesAll = $using:arrayFeaturesAll - $htSubscriptionTagList = $using:htSubscriptionTagList - $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource - $htAllTagList = $using:htAllTagList - $htSubscriptionTags = $using:htSubscriptionTags - $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy - $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet - $htCacheDefinitionsRole = $using:htCacheDefinitionsRole - $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint - $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy - $htCachePolicyComplianceSUB = $using:htCachePolicyComplianceSUB - $htCachePolicyComplianceResponseTooLargeSUB = $using:htCachePolicyComplianceResponseTooLargeSUB - $htCacheAssignmentsRole = $using:htCacheAssignmentsRole - $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources - $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint - $htCacheAssignmentsPolicyOnResourceGroupsAndResources = $using:htCacheAssignmentsPolicyOnResourceGroupsAndResources - $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy - $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions - $htResourceLocks = $using:htResourceLocks - $LimitPOLICYPolicyDefinitionsScopedSubscription = $using:LimitPOLICYPolicyDefinitionsScopedSubscription - $LimitPOLICYPolicySetDefinitionsScopedSubscription = $using:LimitPOLICYPolicySetDefinitionsScopedSubscription - $LimitPOLICYPolicyAssignmentsSubscription = $using:LimitPOLICYPolicyAssignmentsSubscription - $LimitPOLICYPolicySetAssignmentsSubscription = $using:LimitPOLICYPolicySetAssignmentsSubscription - $childrenSubscriptionsCount = $using:childrenSubscriptionsCount - $subsToProcessInCustomDataCollectionCount = $using:subsToProcessInCustomDataCollectionCount - $arrayDataCollectionProgressSub = $using:arrayDataCollectionProgressSub - $arraySubResourcesAddArrayDuration = $using:arraySubResourcesAddArrayDuration - $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI - $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI - $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub - $htMgASCSecureScore = $using:htMgASCSecureScore - $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention - $htNamingValidation = $using:htNamingValidation - $htPrincipals = $using:htPrincipals - $htServicePrincipals = $using:htServicePrincipals - $htUserTypesGuest = $using:htUserTypesGuest - $arrayDefenderPlans = $using:arrayDefenderPlans - $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped - $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources - $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit - $arrayPsRule = $using:arrayPsRule - $arrayPSRuleTracking = $using:arrayPSRuleTracking - $htClassicAdministrators = $using:htClassicAdministrators - $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM - $alzPolicies = $using:alzPolicies - $alzPolicySets = $using:alzPolicySets - $alzPolicyHashes = $using:alzPolicyHashes - $alzPolicySetHashes = $using:alzPolicySetHashes - $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances - $htDefenderEmailContacts = $using:htDefenderEmailContacts - $arrayVNets = $using:arrayVNets - $arrayPrivateEndPoints = $using:arrayPrivateEndPoints - $htResourceProvidersRef = $using:htResourceProvidersRef - $arrayPrivateEndPointsFromResourceProperties = $using:arrayPrivateEndPointsFromResourceProperties - $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed - $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes - $arrayAdvisorScores = $using:arrayAdvisorScores - $ValidPolicyEffects = $using:ValidPolicyEffects - #$htResourcesWithProperties = $using:htResourcesWithProperties - #other - $function:addRowToTable = $using:funcAddRowToTable - $function:namingValidation = $using:funcNamingValidation - $function:resolveObjectIds = $using:funcResolveObjectIds - $function:testGuid = $using:funcTestGuid - $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore - $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans - $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub - $function:dataCollectionResources = $using:funcDataCollectionResources - $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts - $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups - $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders - $function:dataCollectionFeatures = $using:funcDataCollectionFeatures - $function:dataCollectionResourceLocks = $using:funcDataCollectionResourceLocks - $function:dataCollectionTags = $using:funcDataCollectionTags - $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates - $function:dataCollectionASCSecureScoreSub = $using:funcDataCollectionASCSecureScoreSub - $function:dataCollectionBluePrintDefinitionsSub = $using:funcDataCollectionBluePrintDefinitionsSub - $function:dataCollectionBluePrintAssignmentsSub = $using:funcDataCollectionBluePrintAssignmentsSub - $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions - $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions - $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions - $function:dataCollectionPolicyAssignmentsSub = $using:funcDataCollectionPolicyAssignmentsSub - $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions - $function:dataCollectionRoleAssignmentsSub = $using:funcDataCollectionRoleAssignmentsSub - $function:dataCollectionClassicAdministratorsSub = $using:funcDataCollectionClassicAdministratorsSub - $function:dataCollectionDefenderEmailContacts = $using:funcDataCollectionDefenderEmailContacts - $function:dataCollectionVNets = $using:funcDataCollectionVNets - $function:dataCollectionPrivateEndpoints = $using:funcDataCollectionPrivateEndpoints - $function:dataCollectionAdvisorScores = $using:funcDataCollectionAdvisorScores - $function:detectPolicyEffect = $using:funcDetectPolicyEffect - #endregion UsingVARs + $subsToProcessInCustomDataCollectionBatch = ($subsToProcessInCustomDataCollection) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($subsToProcessInCustomDataCollectionBatch.Count) batches" + + $startBatch = Get-Date + showMemoryUsage + + $subsToProcessInCustomDataCollectionBatch | ForEach-Object -Parallel { + $startSubLoopThis = Get-Date + #region UsingVARs + #Parameters MG&Sub related + $CsvDelimiter = $using:CsvDelimiter + $CsvDelimiterOpposite = $using:CsvDelimiterOpposite + #Parameters Sub related + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $newTable = $using:newTable + $storageAccounts = $using:storageAccounts + $resourcesAll = $using:resourcesAll + $resourcesIdsAll = $using:resourcesIdsAll + $resourceGroupsAll = $using:resourceGroupsAll + $customDataCollectionDuration = $using:customDataCollectionDuration + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htManagementGroupsMgPath = $using:htManagementGroupsMgPath + $htResourceProvidersAll = $using:htResourceProvidersAll + $arrayFeaturesAll = $using:arrayFeaturesAll + $htSubscriptionTagList = $using:htSubscriptionTagList + $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource + $htAllTagList = $using:htAllTagList + $htSubscriptionTags = $using:htSubscriptionTags + $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy + $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet + $htCacheDefinitionsRole = $using:htCacheDefinitionsRole + $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint + $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy + $htCachePolicyComplianceSUB = $using:htCachePolicyComplianceSUB + $htCachePolicyComplianceResponseTooLargeSUB = $using:htCachePolicyComplianceResponseTooLargeSUB + $htCacheAssignmentsRole = $using:htCacheAssignmentsRole + $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources + $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint + $htCacheAssignmentsPolicyOnResourceGroupsAndResources = $using:htCacheAssignmentsPolicyOnResourceGroupsAndResources + $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy + $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions + $htResourceLocks = $using:htResourceLocks + $LimitPOLICYPolicyDefinitionsScopedSubscription = $using:LimitPOLICYPolicyDefinitionsScopedSubscription + $LimitPOLICYPolicySetDefinitionsScopedSubscription = $using:LimitPOLICYPolicySetDefinitionsScopedSubscription + $LimitPOLICYPolicyAssignmentsSubscription = $using:LimitPOLICYPolicyAssignmentsSubscription + $LimitPOLICYPolicySetAssignmentsSubscription = $using:LimitPOLICYPolicySetAssignmentsSubscription + $childrenSubscriptionsCount = $using:childrenSubscriptionsCount + $subsToProcessInCustomDataCollectionCount = $using:subsToProcessInCustomDataCollectionCount + $arrayDataCollectionProgressSub = $using:arrayDataCollectionProgressSub + $arraySubResourcesAddArrayDuration = $using:arraySubResourcesAddArrayDuration + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI + $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub + $htMgASCSecureScore = $using:htMgASCSecureScore + $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention + $htNamingValidation = $using:htNamingValidation + $htPrincipals = $using:htPrincipals + $htServicePrincipals = $using:htServicePrincipals + $htUserTypesGuest = $using:htUserTypesGuest + $arrayDefenderPlans = $using:arrayDefenderPlans + $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped + $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources + $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit + $arrayPsRule = $using:arrayPsRule + $arrayPSRuleTracking = $using:arrayPSRuleTracking + $htClassicAdministrators = $using:htClassicAdministrators + $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM + $alzPolicies = $using:alzPolicies + $alzPolicySets = $using:alzPolicySets + $alzPolicyHashes = $using:alzPolicyHashes + $alzPolicySetHashes = $using:alzPolicySetHashes + $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances + $htDefenderEmailContacts = $using:htDefenderEmailContacts + $arrayVNets = $using:arrayVNets + $arrayPrivateEndPoints = $using:arrayPrivateEndPoints + $htResourceProvidersRef = $using:htResourceProvidersRef + $arrayPrivateEndPointsFromResourceProperties = $using:arrayPrivateEndPointsFromResourceProperties + $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed + $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes + $arrayAdvisorScores = $using:arrayAdvisorScores + $ValidPolicyEffects = $using:ValidPolicyEffects + #$htResourcesWithProperties = $using:htResourcesWithProperties + #other + $function:addRowToTable = $using:funcAddRowToTable + $function:namingValidation = $using:funcNamingValidation + $function:resolveObjectIds = $using:funcResolveObjectIds + $function:testGuid = $using:funcTestGuid + $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore + $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans + $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub + $function:dataCollectionResources = $using:funcDataCollectionResources + $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts + $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups + $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders + $function:dataCollectionFeatures = $using:funcDataCollectionFeatures + $function:dataCollectionResourceLocks = $using:funcDataCollectionResourceLocks + $function:dataCollectionTags = $using:funcDataCollectionTags + $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates + $function:dataCollectionASCSecureScoreSub = $using:funcDataCollectionASCSecureScoreSub + $function:dataCollectionBluePrintDefinitionsSub = $using:funcDataCollectionBluePrintDefinitionsSub + $function:dataCollectionBluePrintAssignmentsSub = $using:funcDataCollectionBluePrintAssignmentsSub + $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions + $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions + $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions + $function:dataCollectionPolicyAssignmentsSub = $using:funcDataCollectionPolicyAssignmentsSub + $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions + $function:dataCollectionRoleAssignmentsSub = $using:funcDataCollectionRoleAssignmentsSub + $function:dataCollectionClassicAdministratorsSub = $using:funcDataCollectionClassicAdministratorsSub + $function:dataCollectionDefenderEmailContacts = $using:funcDataCollectionDefenderEmailContacts + $function:dataCollectionVNets = $using:funcDataCollectionVNets + $function:dataCollectionPrivateEndpoints = $using:funcDataCollectionPrivateEndpoints + $function:dataCollectionAdvisorScores = $using:funcDataCollectionAdvisorScores + $function:detectPolicyEffect = $using:funcDetectPolicyEffect + #endregion UsingVARs + + foreach ($childMgSubDetail in $_.Group) { $addRowToTableDone = $false @@ -610,12 +612,11 @@ function processDataCollection { $null = $script:arrayDataCollectionProgressSub.Add($childMgSubId) $progressCount = ($arrayDataCollectionProgressSub).Count Write-Host " $($progressCount)/$($subsToProcessInCustomDataCollectionCount) Subscriptions processed" + } + } -ThrottleLimit $ThrottleLimit - } -ThrottleLimit $ThrottleLimit - - $endBatch = Get-Date - Write-Host " Batch #$batchCnt processing duration: $((New-TimeSpan -Start $startBatch -End $endBatch).TotalMinutes) minutes ($((New-TimeSpan -Start $startBatch -End $endBatch).TotalSeconds) seconds)" - } + $endBatch = Get-Date + Write-Host " Batch #$batchCnt processing duration: $((New-TimeSpan -Start $startBatch -End $endBatch).TotalMinutes) minutes ($((New-TimeSpan -Start $startBatch -End $endBatch).TotalSeconds) seconds)" $endSubLoop = Get-Date Write-Host " CustomDataCollection Subscriptions processing duration: $((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalSeconds) seconds)" @@ -625,7 +626,6 @@ function processDataCollection { Write-Host " CustomDataCollection Subscriptions 'PSRule for Azure' processing duration (in sum): $($durationPSRuleTotalSeconds / 60) minutes ($($durationPSRuleTotalSeconds) seconds)" } } - #test Write-Host " built-in PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'BuiltIn'}).Count)" Write-Host " custom PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'Custom'}).Count)" Write-Host " all PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.Count)" diff --git a/pwsh/dev/functions/processDefinitionInsights.ps1 b/pwsh/dev/functions/processDefinitionInsights.ps1 index 3361cf33..16f2d925 100644 --- a/pwsh/dev/functions/processDefinitionInsights.ps1 +++ b/pwsh/dev/functions/processDefinitionInsights.ps1 @@ -57,9 +57,8 @@ function processDefinitionInsights() { ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments) } else { - $array = @() - $array += ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments - $array += $htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments + $array = [System.Collections.ArrayList]@() + $null = $array.Add($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments) ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = $array } } @@ -72,9 +71,8 @@ function processDefinitionInsights() { ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments) } else { - $array = @() - $array += ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments - $array += $htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments + $array = [System.Collections.ArrayList]@() + $null = $array.Add($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments) ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = $array } } @@ -843,7 +841,7 @@ tf.init();}} $null = $arrayRoleDefinitionsForCSVExport.Add([PSCustomObject]@{ Name = $role.Name Id = $role.Id - Description = $role.Json.description + Description = $role.Json.properties.description Type = $roleType AssignmentsCount = $assignmentsCount AssignableScopesCount = $AssignableScopesCount diff --git a/pwsh/dev/functions/processNetwork.ps1 b/pwsh/dev/functions/processNetwork.ps1 index 8ca07356..2b2257fc 100644 --- a/pwsh/dev/functions/processNetwork.ps1 +++ b/pwsh/dev/functions/processNetwork.ps1 @@ -76,7 +76,7 @@ function processNetwork { } } else { - $arrayRemoteMGPath = @() + $arrayRemoteMGPath = [System.Collections.ArrayList]@() foreach ($remoteId in $remoteTenantId) { if ($remoteId -eq 'SubscriptionNotFound Tenant unknown') { $remoteMGPath = 'unknown' @@ -86,10 +86,10 @@ function processNetwork { $objectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" + $null = $arrayRemoteMGPath.Add("$remoteId (MS)") } else { - $arrayRemoteMGPath += $remoteId + $null = $arrayRemoteMGPath.Add($remoteId) } if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { $peeringXTenant = 'false' diff --git a/pwsh/dev/functions/processPrivateEndpoints.ps1 b/pwsh/dev/functions/processPrivateEndpoints.ps1 index 7344e782..f54eedcb 100644 --- a/pwsh/dev/functions/processPrivateEndpoints.ps1 +++ b/pwsh/dev/functions/processPrivateEndpoints.ps1 @@ -39,15 +39,15 @@ function processPrivateEndpoints { else { $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($peSubscriptionId)?api-version=2020-01-01" $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($peSubscriptionId)'" - $arrayRemoteMGPath = @() + $arrayRemoteMGPath = [System.Collections.ArrayList]@() foreach ($remoteId in $remoteTenantId) { $objectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" + $null = $arrayRemoteMGPath.Add("$remoteId (MS)") } else { - $arrayRemoteMGPath += $remoteId + $null = $arrayRemoteMGPath.Add($remoteId) } if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { $peXTenant = $false @@ -198,15 +198,15 @@ function processPrivateEndpoints { else { $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($resourceSubscriptionId)?api-version=2020-01-01" $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($resourceSubscriptionId)'" - $arrayRemoteMGPath = @() + $arrayRemoteMGPath = [System.Collections.ArrayList]@() foreach ($remoteId in $remoteTenantId) { $objectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { if ($remoteId -in $MSTenantIds) { - $arrayRemoteMGPath += "$remoteId (MS)" + $null = $arrayRemoteMGPath.Add("$remoteId (MS)") } else { - $arrayRemoteMGPath += $remoteId + $null = $arrayRemoteMGPath.Add($remoteId) } if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { $resourceXTenant = $false diff --git a/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 b/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 index 4d5a5d61..ddadf898 100644 --- a/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 +++ b/pwsh/dev/functions/processScopeInsightsMgOrSub.ps1 @@ -145,7 +145,7 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc - + @@ -173,18 +173,18 @@ function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subsc $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +
    "@) if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Container registries' docs
    +    Using deprecated plan 'Container registries' learn
    '@) } if ($defenderPlanSubscriptionDeprecatedKubernetesService) { [void]$htmlScopeInsights.AppendLine(@' -    Using deprecated plan 'Kubernetes' docs
    +    Using deprecated plan 'Kubernetes' learn
    '@) } @@ -286,7 +286,7 @@ tf.init();}} if ($subscriptionSkippedMDfC.Count -gt 0) { if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { [void]$htmlScopeInsights.AppendLine(@" - Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) learn "@) } else { @@ -298,7 +298,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Microsoft Defender for Cloud plans docs + No Microsoft Defender for Cloud plans learn '@) } } @@ -440,7 +440,7 @@ tf.init();}} } else { [void]$htmlScopeInsights.AppendLine(@' - No Subscription Diagnostic settings docs + No Subscription Diagnostic settings learn '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -562,7 +562,7 @@ extensions: [{ name: 'sort' }]
    -   Resource naming and tagging decision guide docs
    +   Resource naming and tagging decision guide learn
       Download CSV semicolon | comma

    Default Management Group docs

    Default Management Group learn

    Default Management Group docs

    Default Management Group learn

    Subscription Path: $subPath
    State: $subscriptionState
    QuotaId: $subscriptionQuotaId
    Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs
    Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , learn
    Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState
    Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity
    Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles
    @@ -636,7 +636,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@" - Tag Name Usage ($tagsUsageCount Tags) docs + Tag Name Usage ($tagsUsageCount Tags) learn "@) } [void]$htmlScopeInsights.AppendLine(@' @@ -920,7 +920,7 @@ extensions: [{ name: 'sort' }]
    -   Set up preview features in Azure subscription docs +   Set up preview features in Azure subscription learn
    @@ -986,7 +986,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 enabled Subscription Features docs + 0 enabled Subscription Features learn '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -1013,7 +1013,7 @@ extensions: [{ name: 'sort' }]
    -   Considerations before applying locks docs +   Considerations before applying locks learn
    @@ -1081,7 +1081,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Resource Locks docs + 0 Resource Locks learn '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -1099,7 +1099,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@" - +
    $(($mgAllChildMgs).count -1) ManagementGroups below this scope
    $(($mgAllChildSubscriptions).count) Subscriptions below this scope
    Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs
    Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , learn
    "@) @@ -1235,7 +1235,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - No Management Group Diagnostic settings docs + No Management Group Diagnostic settings learn '@) } #endregion ScopeInsightsDiagnosticsMg @@ -1610,7 +1610,7 @@ extensions: [{ name: 'sort' }] [void]$htmlScopeInsights.AppendLine(@"
    -   CAF - Recommended abbreviations for Azure resource types docs
    +   CAF - Recommended abbreviations for Azure resource types learn
       Resource details can be found in the CSV output *_ResourcesAll.csv
       Download CSV semicolon | comma @@ -2161,7 +2161,7 @@ extensions: [{ name: 'sort' }]
    -   Managed identity 'user-assigned' vs 'system-assigned' docs
    +   Managed identity 'user-assigned' vs 'system-assigned' learn
       Download CSV semicolon | comma
    diff --git a/pwsh/dev/functions/processStorageAccountAnalysis.ps1 b/pwsh/dev/functions/processStorageAccountAnalysis.ps1 index 3ed16835..25270e26 100644 --- a/pwsh/dev/functions/processStorageAccountAnalysis.ps1 +++ b/pwsh/dev/functions/processStorageAccountAnalysis.ps1 @@ -22,8 +22,13 @@ function processStorageAccountAnalysis { } } - $storageAccounts | ForEach-Object -Parallel { - $storageAccount = $_ + $batchSize = [math]::ceiling($storageAccounts.Count / $ThrottleLimit) + Write-Host "Optimal batch size: $($batchSize)" + $counterBatch = [PSCustomObject] @{ Value = 0 } + $storageAccountsBatch = ($storageAccounts) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + Write-Host "Processing data in $($storageAccountsBatch.Count) batches" + + $storageAccountsBatch | ForEach-Object -Parallel { $azAPICallConf = $using:azAPICallConf $arrayStorageAccountAnalysisResults = $using:arrayStorageAccountAnalysisResults $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI @@ -33,317 +38,320 @@ function processStorageAccountAnalysis { $htSACost = $using:htSACost $StorageAccountAccessAnalysisSubscriptionTags = $using:StorageAccountAccessAnalysisSubscriptionTags $StorageAccountAccessAnalysisStorageAccountTags = $using:StorageAccountAccessAnalysisStorageAccountTags - $listContainersSuccess = 'n/a' - $containersCount = 'n/a' - $arrayContainers = @() - $arrayContainersAnonymousContainer = @() - $arrayContainersAnonymousBlob = @() - $staticWebsitesState = 'n/a' - $webSiteResponds = 'n/a' - $subscriptionId = ($storageAccount.SA.id -split '/')[2] - $resourceGroupName = ($storageAccount.SA.id -split '/')[4] - $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails - Write-Host "Processing Storage Account '$($storageAccount.SA.name)' - Subscription: '$($subDetails.displayName)' ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)]" + foreach ($storageAccount in $_.Group) { + $listContainersSuccess = 'n/a' + $containersCount = 'n/a' + $arrayContainers = [System.Collections.ArrayList]@() + $arrayContainersAnonymousContainer = [System.Collections.ArrayList]@() + $arrayContainersAnonymousBlob = [System.Collections.ArrayList]@() + $staticWebsitesState = 'n/a' + $webSiteResponds = 'n/a' - if ($storageAccount.SA.Properties.primaryEndpoints.blob) { + $subscriptionId = ($storageAccount.SA.id -split '/')[2] + $resourceGroupName = ($storageAccount.SA.id -split '/')[4] + $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails - $urlServiceProps = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?restype=service&comp=properties" - $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get restype=service&comp=properties" -saResourceGroupName $resourceGroupName -unhandledErrorAction Continue - if ($saProperties) { - if ($saProperties -eq 'AuthorizationFailure' -or $saProperties -eq 'AuthorizationPermissionDenied' -or $saProperties -eq 'ResourceUnavailable' -or $saProperties -eq 'AuthorizationPermissionMismatch' ) { - if ($saProperties -eq 'ResourceUnavailable') { - $staticWebsitesState = $saProperties - } - } - else { - try { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if ($saProperties.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$saProperties - $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) - } + Write-Host "Processing Storage Account '$($storageAccount.SA.name)' - Subscription: '$($subDetails.displayName)' ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)]" - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions - if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { - if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { - $staticWebsitesState = $true + if ($storageAccount.SA.Properties.primaryEndpoints.blob) { + + $urlServiceProps = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?restype=service&comp=properties" + $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get restype=service&comp=properties" -saResourceGroupName $resourceGroupName -unhandledErrorAction Continue + if ($saProperties) { + if ($saProperties -eq 'AuthorizationFailure' -or $saProperties -eq 'AuthorizationPermissionDenied' -or $saProperties -eq 'ResourceUnavailable' -or $saProperties -eq 'AuthorizationPermissionMismatch' ) { + if ($saProperties -eq 'ResourceUnavailable') { + $staticWebsitesState = $saProperties + } + } + else { + try { + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($saProperties.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$saProperties + $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) } - else { - $staticWebsitesState = $false + + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { + $staticWebsitesState = $true + } + else { + $staticWebsitesState = $false + } } } - } - catch { - Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.SA.name)" - Write-Host $($saProperties.ForEach({ [char]$_ }) -join '') -ForegroundColor Cyan + catch { + Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.SA.name)" + Write-Host $($saProperties.ForEach({ [char]$_ }) -join '') -ForegroundColor Cyan + } } } - } - $urlCompList = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?comp=list" - $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get comp=list" -unhandledErrorAction Continue - if ($listContainers) { - if ($listContainers -eq 'AuthorizationFailure' -or $listContainers -eq 'AuthorizationPermissionDenied' -or $listContainers -eq 'ResourceUnavailable' -or $listContainers -eq 'AuthorizationPermissionMismatch') { - if ($listContainers -eq 'ResourceUnavailable') { - $listContainersSuccess = $listContainers + $urlCompList = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?comp=list" + $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get comp=list" -unhandledErrorAction Continue + if ($listContainers) { + if ($listContainers -eq 'AuthorizationFailure' -or $listContainers -eq 'AuthorizationPermissionDenied' -or $listContainers -eq 'ResourceUnavailable' -or $listContainers -eq 'AuthorizationPermissionMismatch') { + if ($listContainers -eq 'ResourceUnavailable') { + $listContainersSuccess = $listContainers + } + else { + $listContainersSuccess = $false + } } else { - $listContainersSuccess = $false + $listContainersSuccess = $true } - } - else { - $listContainersSuccess = $true - } - if ($listContainersSuccess -eq $true) { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if ($listContainers.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$listContainers - $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) - } + if ($listContainersSuccess -eq $true) { + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($listContainers.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$listContainers + $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) + } - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions - $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions + $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count - foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { - $arrayContainers += $container.Name - if ($container.Name -eq '$web' -and $staticWebsitesState) { - if ($storageAccount.SA.properties.primaryEndpoints.web) { - try { - $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.SA.properties.primaryEndpoints.web -Method 'HEAD' - $webSiteResponds = $true - } - catch { - $webSiteResponds = $false + foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { + $null = $arrayContainers.Add($container.Name) + if ($container.Name -eq '$web' -and $staticWebsitesState) { + if ($storageAccount.SA.properties.primaryEndpoints.web) { + try { + $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.SA.properties.primaryEndpoints.web -Method 'HEAD' + $webSiteResponds = $true + } + catch { + $webSiteResponds = $false + } } } - } - if ($container.Properties.PublicAccess) { - if ($container.Properties.PublicAccess -eq 'blob') { - $arrayContainersAnonymousBlob += $container.Name - } - if ($container.Properties.PublicAccess -eq 'container') { - $arrayContainersAnonymousContainer += $container.Name + if ($container.Properties.PublicAccess) { + if ($container.Properties.PublicAccess -eq 'blob') { + $null = $arrayContainersAnonymousBlob.Add($container.Name) + } + if ($container.Properties.PublicAccess -eq 'container') { + $null = $arrayContainersAnonymousContainer.Add($container.Name) + } } } } } } - } - $allowSharedKeyAccess = $storageAccount.SA.properties.allowSharedKeyAccess - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowSharedKeyAccess)) { - $allowSharedKeyAccess = 'likely True' - } - $requireInfrastructureEncryption = $storageAccount.SA.properties.encryption.requireInfrastructureEncryption - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.encryption.requireInfrastructureEncryption)) { - $requireInfrastructureEncryption = 'likely False' - } + $allowSharedKeyAccess = $storageAccount.SA.properties.allowSharedKeyAccess + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowSharedKeyAccess)) { + $allowSharedKeyAccess = 'likely True' + } + $requireInfrastructureEncryption = $storageAccount.SA.properties.encryption.requireInfrastructureEncryption + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.encryption.requireInfrastructureEncryption)) { + $requireInfrastructureEncryption = 'likely False' + } - $arrayResourceAccessRules = [System.Collections.ArrayList]@() - if ($storageAccount.SA.properties.networkAcls.resourceAccessRules) { - if ($storageAccount.SA.properties.networkAcls.resourceAccessRules.count -gt 0) { - foreach ($resourceAccessRule in $storageAccount.SA.properties.networkAcls.resourceAccessRules) { + $arrayResourceAccessRules = [System.Collections.ArrayList]@() + if ($storageAccount.SA.properties.networkAcls.resourceAccessRules) { + if ($storageAccount.SA.properties.networkAcls.resourceAccessRules.count -gt 0) { + foreach ($resourceAccessRule in $storageAccount.SA.properties.networkAcls.resourceAccessRules) { - $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' - $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" + $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' + $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" - [regex]$regex = '\*+' - #$resourceAccessRule.resourceId - switch ($regex.matches($resourceAccessRule.resourceId).count) { - { $_ -eq 1 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'resourceGroup' - sort = 3 - }) - } - { $_ -eq 2 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'subscription' - sort = 2 - }) - } - { $_ -eq 3 } { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'tenant' - sort = 1 - }) - } - default { - $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ - resourcetype = $resourceType - range = 'resource' - resource = $resourceAccessRule.resourceId - sort = 0 - }) + [regex]$regex = '\*+' + #$resourceAccessRule.resourceId + switch ($regex.matches($resourceAccessRule.resourceId).count) { + { $_ -eq 1 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resourceGroup' + sort = 3 + }) + } + { $_ -eq 2 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'subscription' + sort = 2 + }) + } + { $_ -eq 3 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'tenant' + sort = 1 + }) + } + default { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resource' + resource = $resourceAccessRule.resourceId + sort = 0 + }) + } } } } } - } - $resourceAccessRulesCount = $arrayResourceAccessRules.count - if ($resourceAccessRulesCount -eq 0) { - $resourceAccessRules = '' - } - else { - $ht = @{} - foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { + $resourceAccessRulesCount = $arrayResourceAccessRules.count + if ($resourceAccessRulesCount -eq 0) { + $resourceAccessRules = '' + } + else { + $ht = @{} + foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { - if ($accessRulePerRange.Name -eq 'resource') { - $arrayResources = @() - foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { - $arrayResources += $resource + if ($accessRulePerRange.Name -eq 'resource') { + $arrayResources = [System.Collections.ArrayList]@() + foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { + $null = $arrayResources.Add($resource) + } + $ht.($accessRulePerRange.Name) = ($arrayResources) } - $ht.($accessRulePerRange.Name) = [array]($arrayResources) - } - else { - $arrayResourceTypes = @() - foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { - $arrayResourceTypes += $resourceType + else { + $arrayResourceTypes = [System.Collections.ArrayList]@() + foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { + $null = $arrayResourceTypes.Add($resourceType) + } + $ht.($accessRulePerRange.Name) = ($arrayResourceTypes) } - $ht.($accessRulePerRange.Name) = [array]($arrayResourceTypes) } + $resourceAccessRules = $ht | ConvertTo-Json } - $resourceAccessRules = $ht | ConvertTo-Json - } - - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.publicNetworkAccess)) { - $publicNetworkAccess = 'likely Enabled' - } - else { - $publicNetworkAccess = $storageAccount.SA.properties.publicNetworkAccess - } - - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowedCopyScope)) { - $allowedCopyScope = 'From any Storage Account' - } - else { - $allowedCopyScope = $storageAccount.SA.properties.allowedCopyScope - } - if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowCrossTenantReplication)) { - if ($allowedCopyScope -ne 'From any Storage Account') { - $allowCrossTenantReplication = "likely False (allowedCopyScope=$allowedCopyScope)" + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.publicNetworkAccess)) { + $publicNetworkAccess = 'likely Enabled' } else { - $allowCrossTenantReplication = 'likely True' + $publicNetworkAccess = $storageAccount.SA.properties.publicNetworkAccess } - } - else { - $allowCrossTenantReplication = $storageAccount.SA.properties.allowCrossTenantReplication - } - if ($storageAccount.SA.properties.dnsEndpointType) { - $dnsEndpointType = $storageAccount.SA.properties.dnsEndpointType - } - else { - $dnsEndpointType = 'standard' - } + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowedCopyScope)) { + $allowedCopyScope = 'From any Storage Account' + } + else { + $allowedCopyScope = $storageAccount.SA.properties.allowedCopyScope + } - if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { - if ($htSACost.($storageAccount.SA.id)) { - $hlpCost = $htSACost.($storageAccount.SA.id) - $saCost = $hlpCost.costAll - $saCostCurrency = $hlpCost.currencyAll - $saCostMeterCategories = $hlpCost.meterCategoryAll + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowCrossTenantReplication)) { + if ($allowedCopyScope -ne 'From any Storage Account') { + $allowCrossTenantReplication = "likely False (allowedCopyScope=$allowedCopyScope)" + } + else { + $allowCrossTenantReplication = 'likely True' + } } else { - $saCost = 'n/a' - $saCostCurrency = 'n/a' - $saCostMeterCategories = 'n/a' + $allowCrossTenantReplication = $storageAccount.SA.properties.allowCrossTenantReplication } - } - else { - $saCost = '' - $saCostCurrency = '' - $saCostMeterCategories = '' - } - $temp = [System.Collections.ArrayList]@() - $null = $temp.Add([PSCustomObject]@{ - storageAccount = $storageAccount.SA.name - kind = $storageAccount.SA.kind - skuName = $storageAccount.SA.sku.name - skuTier = $storageAccount.SA.sku.tier - location = $storageAccount.SA.location - creationTime = $storageAccount.SA.properties.creationTime - allowBlobPublicAccess = $storageAccount.SA.properties.allowBlobPublicAccess - publicNetworkAccess = $publicNetworkAccess - SubscriptionId = $subscriptionId - SubscriptionName = $subDetails.displayName - subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId - subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' - resourceGroup = $resourceGroupName - networkAclsdefaultAction = $storageAccount.SA.properties.networkAcls.defaultAction - staticWebsitesState = $staticWebsitesState - staticWebsitesResponse = $webSiteResponds - containersCanBeListed = $listContainersSuccess - containersCount = $containersCount - containers = $arrayContainers -join "$CSVDelimiterOpposite " - containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count - containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " - containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count - containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " - ipRulesCount = $storageAccount.SA.properties.networkAcls.ipRules.Count - ipRulesIPAddressList = ($storageAccount.SA.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " - virtualNetworkRulesCount = $storageAccount.SA.properties.networkAcls.virtualNetworkRules.Count - virtualNetworkRulesList = ($storageAccount.SA.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " - resourceAccessRulesCount = $resourceAccessRulesCount - resourceAccessRules = $resourceAccessRules - bypass = ($storageAccount.SA.properties.networkAcls.bypass | Sort-Object) -join "$CSVDelimiterOpposite " - supportsHttpsTrafficOnly = $storageAccount.SA.properties.supportsHttpsTrafficOnly - minimumTlsVersion = $storageAccount.SA.properties.minimumTlsVersion - allowSharedKeyAccess = $allowSharedKeyAccess - requireInfrastructureEncryption = $requireInfrastructureEncryption - allowedCopyScope = $allowedCopyScope - allowCrossTenantReplication = $allowCrossTenantReplication - dnsEndpointType = $dnsEndpointType - usedCapacity = $storageAccount.SAUsedCapacity - cost = $saCost - metercategory = $saCostMeterCategories - curreny = $saCostCurrency - }) + if ($storageAccount.SA.properties.dnsEndpointType) { + $dnsEndpointType = $storageAccount.SA.properties.dnsEndpointType + } + else { + $dnsEndpointType = 'standard' + } - if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { - foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { - if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { - $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + if ($htSACost.($storageAccount.SA.id)) { + $hlpCost = $htSACost.($storageAccount.SA.id) + $saCost = $hlpCost.costAll + $saCostCurrency = $hlpCost.currencyAll + $saCostMeterCategories = $hlpCost.meterCategoryAll } else { - $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + $saCost = 'n/a' + $saCostCurrency = 'n/a' + $saCostMeterCategories = 'n/a' } } - } + else { + $saCost = '' + $saCostCurrency = '' + $saCostMeterCategories = '' + } - if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { - if ($storageAccount.SA.tags) { - $htAllSATags = @{} - foreach ($saTagName in ($storageAccount.SA.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { - $htAllSATags.$saTagName = $storageAccount.SA.tags.$saTagName + $temp = [System.Collections.ArrayList]@() + $null = $temp.Add([PSCustomObject]@{ + storageAccount = $storageAccount.SA.name + kind = $storageAccount.SA.kind + skuName = $storageAccount.SA.sku.name + skuTier = $storageAccount.SA.sku.tier + location = $storageAccount.SA.location + creationTime = $storageAccount.SA.properties.creationTime + allowBlobPublicAccess = $storageAccount.SA.properties.allowBlobPublicAccess + publicNetworkAccess = $publicNetworkAccess + SubscriptionId = $subscriptionId + SubscriptionName = $subDetails.displayName + subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId + subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' + resourceGroup = $resourceGroupName + networkAclsdefaultAction = $storageAccount.SA.properties.networkAcls.defaultAction + staticWebsitesState = $staticWebsitesState + staticWebsitesResponse = $webSiteResponds + containersCanBeListed = $listContainersSuccess + containersCount = $containersCount + containers = $arrayContainers -join "$CSVDelimiterOpposite " + containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count + containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " + containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count + containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " + ipRulesCount = $storageAccount.SA.properties.networkAcls.ipRules.Count + ipRulesIPAddressList = ($storageAccount.SA.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " + virtualNetworkRulesCount = $storageAccount.SA.properties.networkAcls.virtualNetworkRules.Count + virtualNetworkRulesList = ($storageAccount.SA.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " + resourceAccessRulesCount = $resourceAccessRulesCount + resourceAccessRules = $resourceAccessRules + bypass = ($storageAccount.SA.properties.networkAcls.bypass | Sort-Object) -join "$CSVDelimiterOpposite " + supportsHttpsTrafficOnly = $storageAccount.SA.properties.supportsHttpsTrafficOnly + minimumTlsVersion = $storageAccount.SA.properties.minimumTlsVersion + allowSharedKeyAccess = $allowSharedKeyAccess + requireInfrastructureEncryption = $requireInfrastructureEncryption + allowedCopyScope = $allowedCopyScope + allowCrossTenantReplication = $allowCrossTenantReplication + dnsEndpointType = $dnsEndpointType + usedCapacity = $storageAccount.SAUsedCapacity + cost = $saCost + metercategory = $saCostMeterCategories + curreny = $saCostCurrency + }) + + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { + if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } } } - foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { - if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { - $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) + + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + if ($storageAccount.SA.tags) { + $htAllSATags = @{} + foreach ($saTagName in ($storageAccount.SA.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { + $htAllSATags.$saTagName = $storageAccount.SA.tags.$saTagName + } } - else { - $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { + if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } } } - } - - $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) + $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) + } } -ThrottleLimit $ThrottleLimit } else { diff --git a/pwsh/dev/functions/processTenantSummary.ps1 b/pwsh/dev/functions/processTenantSummary.ps1 index 5dbe069a..ec4def9a 100644 --- a/pwsh/dev/functions/processTenantSummary.ps1 +++ b/pwsh/dev/functions/processTenantSummary.ps1 @@ -3129,8 +3129,8 @@ extensions: [{ name: 'sort' }] $policyType = 'unknown' $policy = 'unknown' - $arrayExemptedPolicies = @() - $arrayExemptedPoliciesCSV = @() + $arrayExemptedPolicies = [System.Collections.ArrayList]@() + $arrayExemptedPoliciesCSV = [System.Collections.ArrayList]@() $policiesExempted = $null $policiesExemptedCSV = $null $policiesExemptedCSVCount = $null @@ -3182,8 +3182,8 @@ extensions: [{ name: 'sort' }] } } - $arrayExemptedPolicies += $policyExempted - $arrayExemptedPoliciesCSV += $policyExemptedCSV + $null = $arrayExemptedPolicies.Add($policyExempted) + $null = $arrayExemptedPoliciesCSV.Add($policyExemptedCSV) } $policiesExempted = "$($arrayExemptedPolicies.Count)/$($policiesTotalCount) (
    $(($arrayExemptedPolicies | Sort-Object) -join '
    '))" @@ -3441,7 +3441,9 @@ extensions: [{ name: 'sort' }] #this if (-not $htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower())) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{} + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{ + roleassignments = [System.Collections.ArrayList]@() + } } if (($htCacheDefinitionsRole).($roleAssignment.RoleDefinitionId).IsCustom) { @@ -3460,12 +3462,15 @@ extensions: [{ name: 'sort' }] }) #this - if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments += $array - } - else { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array - } + # if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { + # $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + # else { + # #$script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array + # $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = [System.Collections.ArrayList]@() + # $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) } } @@ -3478,7 +3483,9 @@ extensions: [{ name: 'sort' }] #this if (-not $htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower())) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{} + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{ + roleassignments = [System.Collections.ArrayList]@() + } } if (($htCacheDefinitionsRole).($roleAssignment.RoleDefinitionId).IsCustom) { @@ -3497,12 +3504,15 @@ extensions: [{ name: 'sort' }] }) #this - if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments += $array - } - else { - $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array - } + # if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { + # $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + # else { + # #$script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array + # $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = [System.Collections.ArrayList]@() + # $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) + # } + $null = $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments.Add($array) } } } @@ -3512,61 +3522,81 @@ extensions: [{ name: 'sort' }] #endregion PolicyAssignmentsRoleAssignmentMapping #region PolicyAssignmentsUniqueRelations - $startPolicyAssignmnetsUniqueRelations = Get-Date - Write-Host ' processing PolicyAssignmnetsUniqueRelations' + $startPolicyAssignmentsUniqueRelations = Get-Date + Write-Host ' processing PolicyAssignmentsUniqueRelations' $htPolicyAssignmentRelatedRoleAssignments = @{} $htPolicyAssignmentRelatedExemptions = @{} foreach ($policyAssignmentIdUnique in $policyBaseQueryUniqueAssignments) { #region relatedRoleAssignments - $relatedRoleAssignmentsArray = @() - $relatedRoleAssignmentsArrayClear = @() + $relatedRoleAssignmentsArray = [System.Collections.ArrayList]@() + $relatedRoleAssignmentsArrayClear = [System.Collections.ArrayList]@() if ($htPolicyAssignmentRoleAssignmentMappingCount -gt 0) { - if ($htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId)) { - foreach ($entry in $htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId).roleassignments) { + $policyAssignmentMapping = $htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId) + if ($null -ne $policyAssignmentMapping) { + foreach ($entry in $policyAssignmentMapping.roleassignments) { if ($entry.roleDefinitionType -eq 'builtin') { - $relatedRoleAssignmentsArray += "$($entry.roleDefinitionName) ($($entry.roleAssignmentId))" + $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") } else { - $relatedRoleAssignmentsArray += "$($entry.roleDefinitionName -replace '<', '<' -replace '>', '>') ($($entry.roleAssignmentId))" + $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName -replace '<', '<' -replace '>', '>') ($($entry.roleAssignmentId))") } - $relatedRoleAssignmentsArrayClear += "$($entry.roleDefinitionName) ($($entry.roleAssignmentId))" + $null = $relatedRoleAssignmentsArrayClear.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") } } } + # if ($htPolicyAssignmentRoleAssignmentMappingCount -gt 0) { + # if ($htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId)) { + # foreach ($entry in $htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId).roleassignments) { + # if ($entry.roleDefinitionType -eq 'builtin') { + # $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") + # } + # else { + # $null = $relatedRoleAssignmentsArray.Add("$($entry.roleDefinitionName -replace '<', '<' -replace '>', '>') ($($entry.roleAssignmentId))") + # } + # $null = $relatedRoleAssignmentsArrayClear.Add("$($entry.roleDefinitionName) ($($entry.roleAssignmentId))") + # } + # } + # } + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId) = @{} if (($relatedRoleAssignmentsArray).count -gt 0) { - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = ($relatedRoleAssignmentsArray | Sort-Object) -join "$CsvDelimiterOpposite " - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = ($relatedRoleAssignmentsArrayClear | Sort-Object) -join "$CsvDelimiterOpposite " + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = ($relatedRoleAssignmentsArray | Sort-Object) -join "$CsvDelimiterOpposite " + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = ($relatedRoleAssignmentsArrayClear | Sort-Object) -join "$CsvDelimiterOpposite " + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId) = @{ + relatedRoleAssignments = ($relatedRoleAssignmentsArray | Sort-Object) -join "$CsvDelimiterOpposite " + relatedRoleAssignmentsClear = ($relatedRoleAssignmentsArrayClear | Sort-Object) -join "$CsvDelimiterOpposite " + } } else { - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = 'none' - $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = 'none' + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = 'none' + # $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = 'none' + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId) = @{ + relatedRoleAssignments = 'none' + relatedRoleAssignmentsClear = 'none' + } } #endregion relatedRoleAssignments #region exemptions - $arrayExemptions = @() + $arrayExemptions = [System.Collections.ArrayList]@() foreach ($exemptionId in $htPolicyAssignmentExemptions.keys) { if ($htPolicyAssignmentExemptions.($exemptionId).exemption.properties.policyAssignmentId -eq $policyAssignmentIdUnique.PolicyAssignmentId) { - $arrayExemptions += $htPolicyAssignmentExemptions.($exemptionId).exemption - if (-not $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId)) { - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId) = @{} - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptionsCount = 1 - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptions = $arrayExemptions - } - else { - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptionsCount += 1 - $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptions = $arrayExemptions - } + $null = $arrayExemptions.Add($htPolicyAssignmentExemptions.($exemptionId).exemption) + } + } + if ($arrayExemptions.Count -gt 0) { + $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId) = @{ + exemptionsCount = $arrayExemptions.Count + exemptions = $arrayExemptions } } #endregion exemptions } - $endPolicyAssignmnetsUniqueRelations = Get-Date - Write-Host " PolicyAssignmnetsUniqueRelations processing duration: $((New-TimeSpan -Start $startPolicyAssignmnetsUniqueRelations -End $endPolicyAssignmnetsUniqueRelations).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyAssignmnetsUniqueRelations -End $endPolicyAssignmnetsUniqueRelations).TotalSeconds) seconds)" + $endPolicyAssignmentsUniqueRelations = Get-Date + Write-Host " PolicyAssignmentsUniqueRelations processing duration: $((New-TimeSpan -Start $startPolicyAssignmentsUniqueRelations -End $endPolicyAssignmentsUniqueRelations).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyAssignmentsUniqueRelations -End $endPolicyAssignmentsUniqueRelations).TotalSeconds) seconds)" #endregion PolicyAssignmentsUniqueRelations #region PolicyAssignmentsAllCreateEnriched @@ -6860,14 +6890,14 @@ extensions: [{ name: 'sort' }] #region SUMMARYMGdefault Write-Host ' processing TenantSummary ManagementGroups - default Management Group' [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

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

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

    +

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

    "@) #endregion SUMMARYMGRequireAuthorizationForGroupCreation @@ -6914,8 +6944,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Supported Microsoft Azure offers docs
    - Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
    + Supported Microsoft Azure offers learn
    + Understand Microsoft Defender for Cloud Secure Score Video , Blog , learn
    Download CSV semicolon | comma
    @@ -7317,11 +7347,11 @@ extensions: [{ name: 'sort' }] $tfCount = $tagsUsageCount $htmlTableId = 'TenantSummary_tagsUsage' [void]$htmlTenantSummary.AppendLine(@" - -
    - Resource naming and tagging decision guide docs
    - Download CSV semicolon | comma -
    + +
    + Resource naming and tagging decision guide learn
    + Download CSV semicolon | comma +
    @@ -7394,7 +7424,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

    Tag Name Usage ($tagsUsageCount Tags) docs

    +

    Tag Name Usage ($tagsUsageCount Tags) learn

    "@) } #endregion SUMMARYTagNameUsage @@ -7720,7 +7750,7 @@ extensions: [{ name: 'sort' }]
    - CAF - Recommended abbreviations for Azure resource types docs
    + CAF - Recommended abbreviations for Azure resource types learn
    Resource details can be found in the CSV output *_ResourcesAll.csv
    Download CSV semicolon | comma
    Scope
    @@ -8329,7 +8359,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Set up preview features in Azure subscription docs
    + Set up preview features in Azure subscription learn
    Download CSV semicolon | comma
    @@ -8406,7 +8436,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

    No enabled Subscriptions Features docs

    +

    No enabled Subscriptions Features learn

    '@) } $endSubFeatures = Get-Date @@ -8433,7 +8463,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Considerations before applying locks docs
    + Considerations before applying locks learn
    Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv
    @@ -8502,7 +8532,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

    No Resource Locks at all docs

    +

    No Resource Locks at all learn

    '@) } $endResourceLocks = Get-Date @@ -8522,8 +8552,8 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Register Resource Provider 'Microsoft.Security' docs
    - Microsoft Defender for Cloud's enhanced security features docs
    + Register Resource Provider 'Microsoft.Security' learn
    + Microsoft Defender for Cloud's enhanced security features learn
    Download CSV semicolon | comma
    @@ -8621,17 +8651,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
    + Using deprecated plan 'Container registries'learn
    '@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
    + Using deprecated plan 'Kubernetes'learn
    '@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
    + Microsoft Defender for Cloud's enhanced security featureslearn
    Download CSV semicolon | comma
    @@ -8703,17 +8733,17 @@ paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_ if ($defenderPlanDeprecatedContainerRegistry) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Container registries'docs
    + Using deprecated plan 'Container registries'learn
    '@) } if ($defenderPlanDeprecatedKubernetesService) { [void]$htmlTenantSummary.AppendLine(@' - Using deprecated plan 'Kubernetes'docs
    + Using deprecated plan 'Kubernetes'learn
    '@) } [void]$htmlTenantSummary.AppendLine(@" - Microsoft Defender for Cloud's enhanced security featuresdocs
    + Microsoft Defender for Cloud's enhanced security featureslearn
    Download CSV semicolon | comma
    @@ -8880,7 +8910,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Managed identity 'user-assigned' vs 'system-assigned' docs
    + Managed identity 'user-assigned' vs 'system-assigned' learn
    Download CSV semicolon | comma
    @@ -10345,7 +10375,7 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { [void]$htmlTenantSummary.AppendLine(@"
    - Management Group Diagnostic Settings - Create Or Update - REST API docs
    + Management Group Diagnostic Settings - Create Or Update - REST API learn
    Download CSV semicolon | comma
    @@ -10483,7 +10513,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    No Management Groups configured for Diagnostic settings docs

    +

    No Management Groups configured for Diagnostic settings learn

    '@) } @@ -10492,9 +10522,9 @@ extensions: [{ name: 'sort' }] $tfCount = $arrayMgsWithoutDiagnosticsCount $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' [void]$htmlTenantSummary.AppendLine(@" - +
    - Management Group Diagnostic Settings - Create Or Update - REST API docs
    + Management Group Diagnostic Settings - Create Or Update - REST API learn
    Download CSV semicolon | comma
    @@ -10569,7 +10599,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    All Management Groups are configured for Diagnostic settings docs

    +

    All Management Groups are configured for Diagnostic settings learn

    '@) } #endregion SUMMARYDiagnosticsManagementGroups @@ -10589,7 +10619,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Create diagnostic setting docs
    + Create diagnostic setting learn
    Download CSV semicolon | comma
    @@ -10723,7 +10753,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    No Subscriptions configured for Diagnostic settings docs

    +

    No Subscriptions configured for Diagnostic settings learn

    '@) } @@ -10734,7 +10764,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Create diagnostic setting docs
    + Create diagnostic setting learn
    Download CSV semicolon | comma
    @@ -10809,7 +10839,7 @@ extensions: [{ name: 'sort' }] else { [void]$htmlTenantSummary.AppendLine(@' -

    All Subscriptions are configured for Diagnostic settings docs

    +

    All Subscriptions are configured for Diagnostic settings learn

    '@) } #endregion SUMMARYDiagnosticsSubscriptions @@ -10836,7 +10866,7 @@ extensions: [{ name: 'sort' }]
    Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
    - Supported categories for Azure Resource Logs docs
    + Supported categories for Azure Resource Logs learn
    Download CSV semicolon | comma
    @@ -11160,7 +11190,7 @@ extensions: [{ name: 'sort' }] else { $resourceCount = '0' } - $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check learn " $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ Priority = '2-Medium' PolicyId = 'n/a' @@ -11195,7 +11225,7 @@ extensions: [{ name: 'sort' }]
    Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
    - Supported categories for Azure Resource Logs docs + Supported categories for Azure Resource Logs learn
    @@ -11372,24 +11402,24 @@ extensions: [{ name: 'sort' }] #policySets if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { [void]$htmlTenantSummary.AppendLine(@" -

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

    +

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant learn

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

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

    +

    PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant learn

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

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

    +

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant learn

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

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

    +

    Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant learn

    "@) } @@ -11409,7 +11439,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -11482,7 +11512,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsPolicyAssignments @@ -11496,7 +11526,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -11569,7 +11599,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsPolicyScope @@ -11583,7 +11613,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -11656,7 +11686,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsPolicySetScope @@ -11671,7 +11701,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure RBAC Limits docs
    + Azure RBAC Limits learn
    Download CSV semicolon | comma
    @@ -11744,7 +11774,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYMgsapproachingLimitsRoleAssignment @@ -11765,7 +11795,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Subscription Resource Group Limit docs
    + Azure Subscription Resource Group Limit learn
    Download CSV semicolon | comma
    @@ -11839,7 +11869,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

    + $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups learn

    "@) } #endregion SUMMARYSubsapproachingLimitsResourceGroups @@ -11853,7 +11883,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Subscription Tag Limit docs
    + Azure Subscription Tag Limit learn
    Download CSV semicolon | comma
    @@ -11926,7 +11956,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsSubscriptionTags @@ -11940,7 +11970,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -12013,7 +12043,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsPolicyAssignments @@ -12027,7 +12057,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -12100,7 +12130,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsPolicyScope @@ -12114,7 +12144,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure Policy Limits docs
    + Azure Policy Limits learn
    Download CSV semicolon | comma
    @@ -12187,7 +12217,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

    +

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

    "@) } #endregion SUMMARYSubsapproachingLimitsPolicySetScope @@ -12204,7 +12234,7 @@ extensions: [{ name: 'sort' }] [void]$htmlTenantSummary.AppendLine(@"
    - Azure RBAC Limits docs
    + Azure RBAC Limits learn
    Download CSV semicolon | comma
    @@ -12277,7 +12307,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" - $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

    + $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment learn

    "@) } #endregion SUMMARYSubsapproachingLimitsRoleAssignment diff --git a/version.json b/version.json index a9a93cc6..4cbafd36 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "ProductVersion": "6.3.71" + "ProductVersion": "6.4.0" } \ No newline at end of file From b735d5a0619e3282ccb08e026b782bc8d61ea353 Mon Sep 17 00:00:00 2001 From: Julian Hayward Date: Tue, 6 Feb 2024 21:33:04 +0100 Subject: [PATCH 18/18] 6.4.0 --- README.md | 2 +- history.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b672b0f..1abb7462 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ __Changes__ (2024-Feb-06 / 6.4.0 Minor) * update ARM API-version for RBAC Role definitions. Using `2022-05-01-preview` instead of `2018-11-01-preview` consequently * fix *_roleDefinitions.csv - description partially missing * optimize array handling / best practices -* optimize getting private endpoint capacle resource types / in case resource provider 'microsoft.network' is not registered, try with next available subscription instead of throwing +* optimize getting private endpoint capable resource types / in case resource provider 'microsoft.network' is not registered, try with next available subscription instead of throwing * use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.2.0 * documentation update - style guidance, links updates - kudos @ckittel diff --git a/history.md b/history.md index cfe2dfdf..17ba4add 100644 --- a/history.md +++ b/history.md @@ -11,7 +11,7 @@ __Changes__ (2024-Feb-06 / 6.4.0 Minor) * update ARM API-version for RBAC Role definitions. Using `2022-05-01-preview` instead of `2018-11-01-preview` consequently * fix *_roleDefinitions.csv - description partially missing * optimize array handling / best practices -* optimize getting private endpoint capacle resource types / in case resource provider 'microsoft.network' is not registered, try with next available subscription instead of throwing +* optimize getting private endpoint capable resource types / in case resource provider 'microsoft.network' is not registered, try with next available subscription instead of throwing * use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.2.0 * documentation update - style guidance, links updates - kudos @ckittel