├── .azuredevops └── pipelines │ ├── AzGovViz.pipeline.yml │ └── AzGovViz.variables.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── AzGovViz.yml │ ├── AzGovViz_OIDC.yml │ ├── devskim.yml │ ├── psScriptAnalyzer.yml │ └── scorecard.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── contributionGuide.md ├── history.md ├── img ├── AzDO_Repo-Permissions.png ├── AzDO_md.png ├── AzDO_md_v4.png ├── AzGovVizConnectingDots_v4.2.png ├── DefinitionInsights.png ├── HierarchyMap.png ├── MicrosoftDefenderForCloudCoverage_preview.png ├── PSRuleForAzure_preview.png ├── ScopeInsights.png ├── TenantSummary.png ├── TenantSummary_20221129.png ├── aad850.png ├── aadpermissionsportal_4.jpg ├── azadserviceprincipalinsights73.png ├── azadserviceprincipalinsights_preview_entra-id.png ├── azadvertizer70.png ├── azdo_aad_oidc_0.jpg ├── azdo_aad_oidc_1.jpg ├── azdo_oidc_0.jpg ├── azdo_oidc_1.jpg ├── azgvz_MDfC_securityAlert.png ├── azureappdeployconfig.png ├── buildpipeline.png ├── buildpipeline2.png ├── buildpipeline3.png ├── chatGPT.png ├── codespaces0.png ├── codespaces1.png ├── codespaces2.png ├── codespaces3.png ├── codespaces4.png ├── consumption.png ├── criticalMemoryUsage.png ├── demo4_66.png ├── downloadcsv450.png ├── gitdiff600.jpg ├── identifier.jpg ├── insights_map_pwsh.png ├── jsonfolderfull450.jpg ├── orphanedResourcesCostSavings.png ├── orphaned_stoppedVMs.png ├── permissions.png ├── pimeligibilityIntegrateRoleassignmentsall.png ├── releaseartifactconfig.png ├── releasepipeline.png ├── releaseschedule.png ├── releasetrigger.png ├── stats.jpg ├── webapp_AzDO_yml.png ├── webapp_GitHub_yml.png ├── webapp_authentication.png ├── webapp_configure.png ├── webapp_create.png └── webapp_defaultdocs.png ├── pwsh ├── AzGovVizParallel.ps1 ├── dev │ ├── README.md │ ├── buildAzGovVizParallel.ps1 │ ├── devAzGovVizParallel.ps1 │ └── functions │ │ ├── addHtParameters.ps1 │ │ ├── addIndexNumberToArray.ps1 │ │ ├── addRowToTable.ps1 │ │ ├── apiCallTracking.ps1 │ │ ├── buildJSON.ps1 │ │ ├── buildMD.ps1 │ │ ├── buildPolicyAllJSON.ps1 │ │ ├── buildTree.ps1 │ │ ├── cacheBuiltIn.ps1 │ │ ├── checkAzGovVizVersion.ps1 │ │ ├── createTagList.ps1 │ │ ├── dataCollection │ │ └── dataCollectionFunctions.ps1 │ │ ├── detailSubscriptions.ps1 │ │ ├── detectPolicyEffect.ps1 │ │ ├── exportBaseCSV.ps1 │ │ ├── exportResourceLocks.ps1 │ │ ├── getConsumption.ps1 │ │ ├── getConsumptionv2.ps1 │ │ ├── getDefaultManagementGroup.ps1 │ │ ├── getEntities.ps1 │ │ ├── getFileNaming.ps1 │ │ ├── getGroupmembers.ps1 │ │ ├── getMDfCSecureScoreMG.ps1 │ │ ├── getOrphanedResources.ps1 │ │ ├── getPIMEligible.ps1 │ │ ├── getPolicyHash.ps1 │ │ ├── getPolicyRemediation.ps1 │ │ ├── getPrivateEndpointCapableResourceTypes.ps1 │ │ ├── getResourceDiagnosticsCapability.ps1 │ │ ├── getSubscriptions.ps1 │ │ ├── getTenantDetails.ps1 │ │ ├── handleCloudEnvironment.ps1 │ │ ├── html │ │ └── htmlFunctions.ps1 │ │ ├── namingValidation.ps1 │ │ ├── prepareData.ps1 │ │ ├── processAADGroups.ps1 │ │ ├── processALZPolicyAssignmentsChecker.ps1 │ │ ├── processALZPolicyVersionChecker.ps1 │ │ ├── processApplications.ps1 │ │ ├── processDataCollection.ps1 │ │ ├── processDefinitionInsights.ps1 │ │ ├── processDiagramMermaid.ps1 │ │ ├── processHierarchyMapOnly.ps1 │ │ ├── processHierarchyMapOnlyCustomData.ps1 │ │ ├── processMDfCCoverage.ps1 │ │ ├── processManagedIdentities.ps1 │ │ ├── processNetwork.ps1 │ │ ├── processPrivateEndpoints.ps1 │ │ ├── processScopeInsightsMgOrSub.ps1 │ │ ├── processStorageAccountAnalysis.ps1 │ │ ├── processTenantSummary.ps1 │ │ ├── removeInvalidFileNameChars.ps1 │ │ ├── resolveObjectIds.ps1 │ │ ├── runInfo.ps1 │ │ ├── selectMg.ps1 │ │ ├── setBaseVariablesMG.ps1 │ │ ├── setOutput.ps1 │ │ ├── setTranscript.ps1 │ │ ├── showMemoryUsage.ps1 │ │ ├── stats.ps1 │ │ ├── testGuid.ps1 │ │ ├── testPowerShellVersion.ps1 │ │ ├── validateAccess.ps1 │ │ ├── validateLeastPrivilegeForUser.ps1 │ │ └── verifyModules3rd.ps1 └── prerequisites.ps1 ├── setup.md ├── setup ├── azure-devops.md ├── azure-web-app.md ├── console.md └── github.md ├── slides └── AzGovViz_intro.pdf ├── version.json └── version.txt /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/powershell:latest 2 | 3 | RUN apt-get update \ 4 | && apt-get -y install --no-install-recommends git \ 5 | && apt-get autoremove -y \ 6 | && apt-get clean -y \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | RUN pwsh -c 'Install-Module -Name Az.Accounts -Scope AllUsers -Repository PSGallery -Force' -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AzureGovernanceVisualizer", 3 | "dockerFile": "Dockerfile", 4 | "customizations": { 5 | "vscode": { 6 | "settings": { 7 | "terminal.integrated.defaultProfile.linux": "pwsh" 8 | }, 9 | "extensions": [ 10 | "ms-vscode.powershell", 11 | "analytic-signal.preview-html", 12 | "bierner.markdown-mermaid", 13 | "streetsidesoftware.code-spell-checker", 14 | "yzhang.markdown-all-in-one" 15 | ] 16 | } 17 | }, 18 | "forwardPorts": [] 19 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **AzGovViz version** 11 | please only report bug if you are running the latest version of AzGovViz 12 | 13 | **CodeRunPlatform** 14 | e.g. Console, Azure DevOps, GitHub Action, Cloud Shell, GitHub CodeSpaces .. 15 | 16 | **Describe the bug** 17 | A clear and concise description of what the bug is / Paste the error from the script output (replace your tenantId and subscriptionIds) 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. Screenshot is only an addition paste the error output as text, please. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/workflows/AzGovViz.yml: -------------------------------------------------------------------------------- 1 | # Azure Governance Visualizer v6_major_20230728_1 2 | # First things first: 3 | # 1. Mandatory: define in line 11 4 | # 2. Optional: enable the schedule (line 21,22) 5 | # Documentation: https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting 6 | 7 | name: AzureGovernanceVisualizer 8 | 9 | env: 10 | OutputPath: wiki 11 | ManagementGroupId: #provide the Management Group Id, not the displayName 12 | ScriptDir: pwsh #example: 'my folder\pwsh' or 'my folder/pwsh' 13 | ScriptPrereqFile: prerequisites.ps1 14 | ScriptFile: AzGovVizParallel.ps1 15 | WebAppPublish: false #set to true and define the Web App details in the next 3 lines 16 | WebAppSubscriptionId: e.g. 2674403a-4acd-40e6-a694-2ac7b968761e 17 | WebAppResourceGroup: e.g. MyWebAppResourceGroup 18 | WebAppName: e.g. MyAzGovVizWebApp 19 | #handle the GitHub 100MB file size limit; files in the folder 'OutputPath' hitting the limit will be removed https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github#file-size-limits 20 | #set to true AND uncomment line 80 21 | HandleGitHub100MBFileSizeLimit: false 22 | 23 | on: 24 | #schedule: 25 | # - cron: '30 4 * * *' 26 | 27 | # Allows you to run this workflow manually from the Actions tab 28 | workflow_dispatch: 29 | 30 | jobs: 31 | AzureGovernanceVisualizer: 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 36 | - name: Checkout 37 | uses: actions/checkout@v2 38 | 39 | - name: Connect Azure 40 | uses: azure/login@v2 41 | with: 42 | creds: ${{secrets.CREDS}} 43 | enable-AzPSSession: true 44 | # Create secret CREDS (GitHub/Setting/Secrets) 45 | # CREDS looks like this: 46 | # { 47 | # "tenantId": "", 48 | # "subscriptionId": "", 49 | # "clientId": "", 50 | # "clientSecret": "" 51 | # } 52 | 53 | - name: Check prerequisites 54 | uses: azure/powershell@v1 55 | with: 56 | inlineScript: | 57 | . .\$($env:ScriptDir)\$($env:ScriptPrereqFile) -OutputPath ${env:OutputPath} 58 | azPSVersion: "latest" 59 | 60 | - name: Run Azure Governance Visualizer 61 | uses: azure/powershell@v1 62 | with: 63 | inlineScript: | 64 | . .\$($env:ScriptDir)\$($env:ScriptFile) -ManagementGroupId ${env:ManagementGroupId} -ScriptPath ${env:ScriptDir} -OutputPath ${env:OutputPath} 65 | azPSVersion: "latest" 66 | 67 | - name: Handle GH 100MB file size limit 68 | if: env.HandleGitHub100MBFileSizeLimit == 'true' 69 | shell: pwsh 70 | run: | 71 | Write-Host "Checking files in $($env:OutputPath) for GitHub 100MB file size limit" 72 | Write-Host "Ref: https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github#file-size-limits" 73 | $files = Get-ChildItem -Path $($env:OutputPath) -File -Recurse -ErrorAction Stop 74 | Write-Host "Found total of $($files.Count) files" 75 | $gitHubFileSizeLimit = 100 76 | $largeFiles = $files.where({ $_.Length / 1MB -gt $gitHubFileSizeLimit }) 77 | Write-Host "Found $($largeFiles.Count) files hitting the GitHub file size limit" 78 | foreach ($file in $largeFiles) { 79 | Write-Host "File '$($file.Name)' size $($file.Length / 1MB)MB exceeds the GitHub 100MB file size limit - removing file $($file.FullName)" 80 | #Remove-Item -Path $file.FullName -Force 81 | } 82 | 83 | - name: Push Azure Governance Visualizer output to repository 84 | run: | 85 | git config --global user.email "AzureGovernanceVisualizerGHActions@ghActions.com" 86 | git config --global user.name "$GITHUB_ACTOR" 87 | git config pull.rebase false 88 | git add --all 89 | git commit -m "$GITHUB_WORKFLOW $GITHUB_JOB" 90 | git push 91 | 92 | - name: Publish HTML to WebApp 93 | if: env.WebAppPublish == 'true' 94 | uses: azure/powershell@v1 95 | with: 96 | inlineScript: | 97 | $azAPICallConf = initAzAPICall -DebugAzAPICall $true 98 | $currentTask = "AzAPICall - Check if WebApp ($($env:WebAppName)) has Authentication enabled" 99 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($env:WebAppSubscriptionId)/resourceGroups/$($env:WebAppResourceGroup)/providers/Microsoft.Web/sites/$($env:WebAppName)/config/authsettings/list?api-version=2021-02-01" 100 | $method = 'POST' 101 | $request = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -listenOn 'Content' 102 | 103 | $authStatus = $request.properties.enabled 104 | Write-Host "WebApp ($($env:WebAppName)) has Authentication enabled: $authStatus" 105 | if ($authStatus) { 106 | try { 107 | if (Test-Path -Path "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId)_DefinitionInsights.html") { 108 | try { 109 | Compress-Archive -Path "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).html", "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId)_DefinitionInsights.html" -DestinationPath "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).zip" -ErrorAction Stop 110 | } 111 | catch { 112 | throw 'Make sure that the ManagementGroupId variable in the AzGovViz*.yml has correct casing (Linux!=linuX)' 113 | } 114 | } 115 | else { 116 | try { 117 | Compress-Archive -Path "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).html" -DestinationPath "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).zip" -ErrorAction Stop 118 | } 119 | catch { 120 | throw 'Make sure that the ManagementGroupId variable in the AzGovViz*.yml has correct casing (Linux!=linuX)' 121 | } 122 | } 123 | 124 | $null = Select-AzSubscription -SubscriptionId $($env:WebAppSubscriptionId) 125 | if (Publish-AzWebApp -ResourceGroupName $($env:WebAppResourceGroup) -Name $($env:WebAppName) -ArchivePath "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).zip" -Force) { 126 | Write-Host 'HTML published' 127 | } 128 | } 129 | catch { 130 | $_ 131 | Write-Host 'HTML NOT published' 132 | Write-Host "RBAC Role 'Website Contributor' is required" 133 | exit 1 134 | } 135 | } 136 | else { 137 | Write-Host 'Assuming and insisting that you do not want to publish your tenant insights to the public' 138 | Write-Host "HTML NOT published. Please configure authentication on the Azure Web App ($($env:WebAppName))." 139 | exit 1 140 | } 141 | azPSVersion: "latest" 142 | -------------------------------------------------------------------------------- /.github/workflows/AzGovViz_OIDC.yml: -------------------------------------------------------------------------------- 1 | # Azure Governance Visualizer v6_major_20230728_1 2 | # First things first: 3 | # 1. Mandatory: define in line 11 4 | # 2. Optional: enable the schedule (line 22,23) 5 | # Documentation: https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting 6 | 7 | name: AzureGovernanceVisualizer_OIDC 8 | 9 | env: 10 | OutputPath: wiki 11 | ManagementGroupId: #provide the Management Group Id, not the displayName 12 | ScriptDir: pwsh #example: 'my folder\pwsh' or 'my folder/pwsh' 13 | ScriptPrereqFile: prerequisites.ps1 14 | ScriptFile: AzGovVizParallel.ps1 15 | #optional 16 | WebAppPublish: false #set to true and define the Web App details in the next 3 lines 17 | WebAppSubscriptionId: e.g. 2674403a-4acd-40e6-a694-2ac7b968761e 18 | WebAppResourceGroup: e.g. MyWebAppResourceGroup 19 | WebAppName: e.g. MyAzGovVizWebApp 20 | #handle the GitHub 100MB file size limit; files in the folder 'OutputPath' hitting the limit will be removed https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github#file-size-limits 21 | #set to true AND uncomment line 79 22 | HandleGitHub100MBFileSizeLimit: false 23 | 24 | on: 25 | #schedule: 26 | # - cron: '30 5 * * *' 27 | 28 | # Allows you to run this workflow manually from the Actions tab 29 | workflow_dispatch: 30 | 31 | #requirement OIDC 32 | permissions: 33 | id-token: write 34 | contents: write 35 | 36 | jobs: 37 | AzureGovernanceVisualizer: 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v2 43 | 44 | - name: Connect Azure OIDC 45 | uses: azure/login@v2 46 | with: 47 | client-id: ${{secrets.CLIENT_ID}} #create this secret 48 | tenant-id: ${{secrets.TENANT_ID}} #create this secret 49 | subscription-id: ${{secrets.SUBSCRIPTION_ID}} #create this secret 50 | enable-AzPSSession: true 51 | 52 | - name: Check prerequisites 53 | uses: azure/powershell@v1 54 | with: 55 | inlineScript: | 56 | . .\$($env:ScriptDir)\$($env:ScriptPrereqFile) -OutputPath ${env:OutputPath} 57 | azPSVersion: "latest" 58 | 59 | - name: Run Azure Governance Visualizer 60 | uses: azure/powershell@v1 61 | with: 62 | inlineScript: | 63 | . .\$($env:ScriptDir)\$($env:ScriptFile) -ManagementGroupId ${env:ManagementGroupId} -SubscriptionId4AzContext ${{secrets.SUBSCRIPTION_ID}} -ScriptPath ${env:ScriptDir} -OutputPath ${env:OutputPath} -GitHubActionsOIDC 64 | azPSVersion: "latest" 65 | 66 | - name: Handle GH 100MB file size limit 67 | if: env.HandleGitHub100MBFileSizeLimit == 'true' 68 | shell: pwsh 69 | run: | 70 | Write-Host "Checking files in $($env:OutputPath) for GitHub 100MB file size limit" 71 | Write-Host "Ref: https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github#file-size-limits" 72 | $files = Get-ChildItem -Path $($env:OutputPath) -File -Recurse -ErrorAction Stop 73 | Write-Host "Found total of $($files.Count) files" 74 | $gitHubFileSizeLimit = 100 75 | $largeFiles = $files.where({ $_.Length / 1MB -gt $gitHubFileSizeLimit }) 76 | Write-Host "Found $($largeFiles.Count) files hitting the GitHub file size limit" 77 | foreach ($file in $largeFiles) { 78 | Write-Host "File '$($file.Name)' size $($file.Length / 1MB)MB exceeds the GitHub 100MB file size limit - removing file $($file.FullName)" 79 | #Remove-Item -Path $file.FullName -Force 80 | } 81 | 82 | - name: Push Azure Governance Visualizer output to repository 83 | run: | 84 | git config --global user.email "AzureGovernanceVisualizerGHActions@ghActions.com" 85 | git config --global user.name "azgvz" 86 | git config pull.rebase false 87 | git add --all 88 | git commit -m "$GITHUB_WORKFLOW $GITHUB_JOB" 89 | git push 90 | 91 | #log again to avoid timeout before web publishing 92 | - name: Connect Azure OIDC 93 | if: env.WebAppPublish == 'true' 94 | uses: azure/login@v2 95 | with: 96 | client-id: ${{secrets.CLIENT_ID}} #create this secret (GitHub/Setting/Secrets) 97 | tenant-id: ${{secrets.TENANT_ID}} #create this secret 98 | subscription-id: ${{secrets.SUBSCRIPTION_ID}} #create this secret 99 | enable-AzPSSession: true 100 | 101 | - name: Publish HTML to WebApp 102 | if: env.WebAppPublish == 'true' 103 | uses: azure/powershell@v1 104 | with: 105 | inlineScript: | 106 | $azAPICallConf = initAzAPICall -DebugAzAPICall $true 107 | $currentTask = "AzAPICall - Check if WebApp ($($env:WebAppName)) has Authentication enabled" 108 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($env:WebAppSubscriptionId)/resourceGroups/$($env:WebAppResourceGroup)/providers/Microsoft.Web/sites/$($env:WebAppName)/config/authsettings/list?api-version=2021-02-01" 109 | $method = 'POST' 110 | $request = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -listenOn 'Content' 111 | 112 | $authStatus = $request.properties.enabled 113 | Write-Host "WebApp ($($env:WebAppName)) has Authentication enabled: $authStatus" 114 | if ($authStatus) { 115 | try { 116 | if (Test-Path -Path "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId)_DefinitionInsights.html") { 117 | try { 118 | Compress-Archive -Path "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).html", "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId)_DefinitionInsights.html" -DestinationPath "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).zip" -ErrorAction Stop 119 | } 120 | catch { 121 | throw 'Make sure that the ManagementGroupId variable in the AzGovViz*.yml has correct casing (Linux!=linuX)' 122 | } 123 | } 124 | else { 125 | try { 126 | Compress-Archive -Path "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).html" -DestinationPath "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).zip" -ErrorAction Stop 127 | } 128 | catch { 129 | throw 'Make sure that the ManagementGroupId variable in the AzGovViz*.yml has correct casing (Linux!=linuX)' 130 | } 131 | } 132 | 133 | $null = Select-AzSubscription -SubscriptionId $($env:WebAppSubscriptionId) 134 | if (Publish-AzWebApp -ResourceGroupName $($env:WebAppResourceGroup) -Name $($env:WebAppName) -ArchivePath "$($env:OutputPath)/AzGovViz_$($env:ManagementGroupId).zip" -Force) { 135 | Write-Host 'HTML published' 136 | } 137 | } 138 | catch { 139 | $_ 140 | Write-Host 'HTML NOT published' 141 | Write-Host "RBAC Role 'Website Contributor' is required" 142 | exit 1 143 | } 144 | } 145 | else { 146 | Write-Host 'Assuming and insisting that you do not want to publish your tenant insights to the public' 147 | Write-Host "HTML NOT published. Please configure authentication on the Azure Web App ($($env:WebAppName))." 148 | exit 1 149 | } 150 | azPSVersion: "latest" -------------------------------------------------------------------------------- /.github/workflows/devskim.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: DevSkim 7 | 8 | on: 9 | push: 10 | branches: [ "master" ] 11 | pull_request: 12 | branches: [ "master" ] 13 | # schedule: 14 | # - cron: '28 13 * * 2' 15 | 16 | jobs: 17 | lint: 18 | if: github.repository == 'JulianHayward/Azure-MG-Sub-Governance-Reporting' 19 | name: DevSkim 20 | runs-on: ubuntu-22.04 21 | permissions: 22 | actions: read 23 | contents: read 24 | security-events: write 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | - name: Run DevSkim scanner 30 | uses: microsoft/DevSkim-Action@v1 31 | 32 | - name: Upload DevSkim scan results to GitHub Security tab 33 | uses: github/codeql-action/upload-sarif@v2 34 | with: 35 | sarif_file: devskim-results.sarif 36 | -------------------------------------------------------------------------------- /.github/workflows/psScriptAnalyzer.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # 6 | # https://github.com/microsoft/action-psscriptanalyzer 7 | # For more information on PSScriptAnalyzer in general, see 8 | # https://github.com/PowerShell/PSScriptAnalyzer 9 | 10 | name: PSScriptAnalyzer 11 | 12 | on: 13 | push: 14 | branches: [ "master" ] 15 | pull_request: 16 | branches: [ "master" ] 17 | # schedule: 18 | # - cron: '25 6 * * 1' 19 | 20 | permissions: 21 | contents: read 22 | 23 | jobs: 24 | build: 25 | if: github.repository == 'JulianHayward/Azure-MG-Sub-Governance-Reporting' 26 | permissions: 27 | contents: read # for actions/checkout to fetch code 28 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 29 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 30 | name: PSScriptAnalyzer 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Run PSScriptAnalyzer 36 | uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f 37 | with: 38 | # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. 39 | # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. 40 | path: .\ 41 | recurse: true 42 | # Include your own basic security rules. Removing this option will run all the rules 43 | # includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' 44 | excludeRule: '"PSAvoidUsingWriteHost", "PSUseDeclaredVarsMoreThanAssignments", "PSReviewUnusedParameter", "PSUseOutputTypeCorrectly"' 45 | output: results.sarif 46 | 47 | # Upload the SARIF file generated in the previous step 48 | - name: Upload SARIF results file 49 | uses: github/codeql-action/upload-sarif@v2 50 | with: 51 | sarif_file: results.sarif -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | # schedule: 13 | # - cron: '21 0 * * 1' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | if: github.repository == 'JulianHayward/Azure-MG-Sub-Governance-Reporting' 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 49 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard (optional). 70 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 71 | - name: "Upload to code-scanning" 72 | uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 73 | with: 74 | sarif_file: results.sarif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | demo-output 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.trimTrailingWhitespace": true, 4 | "[markdown]": { 5 | "files.trimTrailingWhitespace": false 6 | }, 7 | "powershell.codeFormatting.addWhitespaceAroundPipe": true, 8 | "powershell.codeFormatting.alignPropertyValuePairs": true, 9 | "powershell.codeFormatting.autoCorrectAliases": true, 10 | "powershell.codeFormatting.ignoreOneLineBlock": true, 11 | "powershell.codeFormatting.newLineAfterCloseBrace": true, 12 | "powershell.codeFormatting.newLineAfterOpenBrace": true, 13 | "powershell.codeFormatting.openBraceOnSameLine": true, 14 | "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", 15 | "powershell.codeFormatting.trimWhitespaceAroundPipe": true, 16 | "powershell.codeFormatting.useConstantStrings": true, 17 | "powershell.codeFormatting.useCorrectCasing": true, 18 | "powershell.codeFormatting.whitespaceAfterSeparator": true, 19 | "powershell.codeFormatting.whitespaceAroundOperator": true, 20 | "powershell.codeFormatting.whitespaceBeforeOpenBrace": true, 21 | "powershell.codeFormatting.whitespaceBeforeOpenParen": true, 22 | "powershell.codeFormatting.whitespaceBetweenParameters": true, 23 | "markdown.extension.toc.unorderedList.marker": "*", 24 | "[powershell]": { 25 | "files.encoding": "utf8bom" 26 | } 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Julian Hayward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This document outlines security procedures for the Azure Governance Visualizer (aka AzGovViz) project. 4 | 5 | We appreciate your dedication to responsible disclosure and will make every effort to acknowledge your contributions. 6 | 7 | ## Supported Versions 8 | 9 | Latest 10 | 11 | ## Reporting a Vulnerability 12 | 13 | We ask that all suspected vulnerabilities be privately and responsibly disclosed via [LinkedIn PN](https://www.linkedin.com/in/julianhayward/). 14 | 15 | Here are some helpful details to include in your report: 16 | 17 | - a detailed description of the issue 18 | - the steps required to reproduce the issue 19 | - versions of the project that may be affected by the issue 20 | - if known, any mitigations for the issue 21 | 22 | If the issue is confirmed, we will release a patch as soon as possible likely within 1 day to 30 days depending on complexity. -------------------------------------------------------------------------------- /contributionGuide.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | 1. Fork the repository. 4 | 2. Create a branch. 5 | 3. Change you working directory to `.\Azure-MG-Sub-Governance-Reporting`. 6 | 4. In the folder `.\pwsh\dev` find the function you intend to work on and apply your changes. 7 | 5. Edit the file `.\pwsh\dev\devAzGovVizParallel.ps1`. 8 | - In the param block update the parameter variable `$ProductVersion` accordingly. 9 | - Note: Do not change anything else in this file if you did not introduce new functions! 10 | 6. Execute `.\pwsh\dev\buildAzGovVizParallel.ps1` - This step will rebuild the main `.\pwsh\AzGovVizParallel.ps1` file, incorporating all changes you did in the `.\pwsh\dev` directory. 11 | 7. Edit the file `.\README.md`. 12 | - Update the region `Release history`, replace the changes from the previous release with your changes. 13 | 8. Edit the file `.\history.md`. 14 | - Copy over text for the change description you just did for the `.\README.md`. 15 | 9. Execute the newly created AzGovViz version to test if it completes successfully by running `.\pwsh\AzGovVizParallel.ps1 -ShowRunIdentifier`. 16 | - From the very last line of the output copy the __run identifier__, you'll need that when you open your pull request. 17 | 10. Commit your changes. 18 | 11. Create a pull request. 19 | - Provide the __run identifier__ in the pull request as a proof of successful test. -------------------------------------------------------------------------------- /img/AzDO_Repo-Permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/AzDO_Repo-Permissions.png -------------------------------------------------------------------------------- /img/AzDO_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/AzDO_md.png -------------------------------------------------------------------------------- /img/AzDO_md_v4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/AzDO_md_v4.png -------------------------------------------------------------------------------- /img/AzGovVizConnectingDots_v4.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/AzGovVizConnectingDots_v4.2.png -------------------------------------------------------------------------------- /img/DefinitionInsights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/DefinitionInsights.png -------------------------------------------------------------------------------- /img/HierarchyMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/HierarchyMap.png -------------------------------------------------------------------------------- /img/MicrosoftDefenderForCloudCoverage_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/MicrosoftDefenderForCloudCoverage_preview.png -------------------------------------------------------------------------------- /img/PSRuleForAzure_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/PSRuleForAzure_preview.png -------------------------------------------------------------------------------- /img/ScopeInsights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/ScopeInsights.png -------------------------------------------------------------------------------- /img/TenantSummary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/TenantSummary.png -------------------------------------------------------------------------------- /img/TenantSummary_20221129.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/TenantSummary_20221129.png -------------------------------------------------------------------------------- /img/aad850.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/aad850.png -------------------------------------------------------------------------------- /img/aadpermissionsportal_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/aadpermissionsportal_4.jpg -------------------------------------------------------------------------------- /img/azadserviceprincipalinsights73.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azadserviceprincipalinsights73.png -------------------------------------------------------------------------------- /img/azadserviceprincipalinsights_preview_entra-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azadserviceprincipalinsights_preview_entra-id.png -------------------------------------------------------------------------------- /img/azadvertizer70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azadvertizer70.png -------------------------------------------------------------------------------- /img/azdo_aad_oidc_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azdo_aad_oidc_0.jpg -------------------------------------------------------------------------------- /img/azdo_aad_oidc_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azdo_aad_oidc_1.jpg -------------------------------------------------------------------------------- /img/azdo_oidc_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azdo_oidc_0.jpg -------------------------------------------------------------------------------- /img/azdo_oidc_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azdo_oidc_1.jpg -------------------------------------------------------------------------------- /img/azgvz_MDfC_securityAlert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azgvz_MDfC_securityAlert.png -------------------------------------------------------------------------------- /img/azureappdeployconfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/azureappdeployconfig.png -------------------------------------------------------------------------------- /img/buildpipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/buildpipeline.png -------------------------------------------------------------------------------- /img/buildpipeline2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/buildpipeline2.png -------------------------------------------------------------------------------- /img/buildpipeline3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/buildpipeline3.png -------------------------------------------------------------------------------- /img/chatGPT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/chatGPT.png -------------------------------------------------------------------------------- /img/codespaces0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/codespaces0.png -------------------------------------------------------------------------------- /img/codespaces1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/codespaces1.png -------------------------------------------------------------------------------- /img/codespaces2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/codespaces2.png -------------------------------------------------------------------------------- /img/codespaces3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/codespaces3.png -------------------------------------------------------------------------------- /img/codespaces4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/codespaces4.png -------------------------------------------------------------------------------- /img/consumption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/consumption.png -------------------------------------------------------------------------------- /img/criticalMemoryUsage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/criticalMemoryUsage.png -------------------------------------------------------------------------------- /img/demo4_66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/demo4_66.png -------------------------------------------------------------------------------- /img/downloadcsv450.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/downloadcsv450.png -------------------------------------------------------------------------------- /img/gitdiff600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/gitdiff600.jpg -------------------------------------------------------------------------------- /img/identifier.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/identifier.jpg -------------------------------------------------------------------------------- /img/insights_map_pwsh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/insights_map_pwsh.png -------------------------------------------------------------------------------- /img/jsonfolderfull450.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/jsonfolderfull450.jpg -------------------------------------------------------------------------------- /img/orphanedResourcesCostSavings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/orphanedResourcesCostSavings.png -------------------------------------------------------------------------------- /img/orphaned_stoppedVMs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/orphaned_stoppedVMs.png -------------------------------------------------------------------------------- /img/permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/permissions.png -------------------------------------------------------------------------------- /img/pimeligibilityIntegrateRoleassignmentsall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/pimeligibilityIntegrateRoleassignmentsall.png -------------------------------------------------------------------------------- /img/releaseartifactconfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/releaseartifactconfig.png -------------------------------------------------------------------------------- /img/releasepipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/releasepipeline.png -------------------------------------------------------------------------------- /img/releaseschedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/releaseschedule.png -------------------------------------------------------------------------------- /img/releasetrigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/releasetrigger.png -------------------------------------------------------------------------------- /img/stats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/stats.jpg -------------------------------------------------------------------------------- /img/webapp_AzDO_yml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/webapp_AzDO_yml.png -------------------------------------------------------------------------------- /img/webapp_GitHub_yml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/webapp_GitHub_yml.png -------------------------------------------------------------------------------- /img/webapp_authentication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/webapp_authentication.png -------------------------------------------------------------------------------- /img/webapp_configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/webapp_configure.png -------------------------------------------------------------------------------- /img/webapp_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/webapp_create.png -------------------------------------------------------------------------------- /img/webapp_defaultdocs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/img/webapp_defaultdocs.png -------------------------------------------------------------------------------- /pwsh/dev/README.md: -------------------------------------------------------------------------------- 1 | contents in the __dev__ directory are for dev/test/build AzGovVizParallel.ps1 -------------------------------------------------------------------------------- /pwsh/dev/buildAzGovVizParallel.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [switch] 3 | $skipVersionCompare 4 | ) 5 | $allFunctionLines = foreach ($file in Get-ChildItem -Path .\pwsh\dev\functions -Recurse -Filter *.ps1) { 6 | Get-Content -LiteralPath $file.FullName 7 | } 8 | $functionCode = $allFunctionLines -join "`n" 9 | $AzGovVizScriptFile = Get-Content -Path .\pwsh\dev\devAzGovVizParallel.ps1 -Raw 10 | 11 | $newContent = @" 12 | 13 | #region Functions 14 | $functionCode 15 | "@ 16 | 17 | $startIndex = $AzGovVizScriptFile.IndexOf('#region Functions') 18 | $endIndex = $AzGovVizScriptFile.IndexOf('#endregion Functions') 19 | 20 | $textBefore = $AzGovVizScriptFile.SubString(0, $startIndex) 21 | $textAfter = $AzGovVizScriptFile.SubString($endIndex) 22 | 23 | $textBefore.TrimEnd(), $newContent, $textAfter | Set-Content -Path .\pwsh\AzGovVizParallel.ps1 -Encoding utf8BOM 24 | 25 | $versionPattern = 'ProductVersion = ' 26 | $versiontxt = (Select-String -Path .\pwsh\AzGovVizParallel.ps1 -Pattern $versionPattern) -replace ".*$versionPattern" -replace "'" -replace ',' 27 | $versiontxtSplitted = $versiontxt -split '\.' 28 | if ($versiontxt.Count -ne 1 -or $versiontxtSplitted.count -ne 3 -or $versiontxtSplitted[0] -notmatch '^\d+$' -or $versiontxtSplitted[1] -notmatch '^\d+$' -or $versiontxtSplitted[2] -notmatch '^\d+$') { 29 | Write-Host "version '$versiontxt' unexpected -> expected e.g. 1.0.0" 30 | throw 31 | } 32 | 33 | if (-not $skipVersionCompare) { 34 | try { 35 | $repoVersionJsonUri = 'https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/master/version.json' 36 | $getRepoVersion = Invoke-WebRequest -Uri $repoVersionJsonUri 37 | $repoVersion = ($getRepoVersion.Content | ConvertFrom-Json).ProductVersion 38 | } 39 | catch { 40 | throw "could not get the version from the repo '$repoVersionJsonUri'" 41 | } 42 | 43 | if ($repoVersion -eq $versiontxt) { 44 | throw "the given version $versiontxt is equal to the current version on the repository $repoVersion" 45 | } 46 | 47 | if ([System.Version]$versiontxt -lt [System.Version]$repoVersion) { 48 | throw "the given productVersion '$versiontxt' is lower than the current version on the repository '$repoVersion'" 49 | } 50 | } 51 | 52 | $versionJson = @{ 53 | ProductVersion = $versiontxt 54 | } 55 | 56 | ($versionJson | ConvertTo-Json) | Set-Content -NoNewline -Path .\version.json 57 | 58 | Write-Host "'AzGovVizParallel.ps1' $versiontxt created" -------------------------------------------------------------------------------- /pwsh/dev/functions/addHtParameters.ps1: -------------------------------------------------------------------------------- 1 | function addHtParameters { 2 | Write-Host 'Add Azure Governance Visualizer htParameters' 3 | if ($LargeTenant -eq $true) { 4 | $script:NoScopeInsights = $true 5 | $NoResourceProvidersAtAll = $true 6 | $PolicyAtScopeOnly = $true 7 | $RBACAtScopeOnly = $true 8 | } 9 | 10 | if ($ManagementGroupsOnly) { 11 | $script:NoSingleSubscriptionOutput = $true 12 | } 13 | 14 | if ($HierarchyMapOnly) { 15 | $NoJsonExport = $true 16 | } 17 | 18 | $script:azAPICallConf['htParameters'] += [ordered]@{ 19 | DoAzureConsumption = [bool]$DoAzureConsumption 20 | DoAzureConsumptionPreviousMonth = [bool]$DoAzureConsumptionPreviousMonth 21 | DoNotIncludeResourceGroupsOnPolicy = [bool]$DoNotIncludeResourceGroupsOnPolicy 22 | DoNotIncludeResourceGroupsAndResourcesOnRBAC = [bool]$DoNotIncludeResourceGroupsAndResourcesOnRBAC 23 | DoNotShowRoleAssignmentsUserData = [bool]$DoNotShowRoleAssignmentsUserData 24 | HierarchyMapOnly = [bool]$HierarchyMapOnly 25 | LargeTenant = [bool]$LargeTenant 26 | ManagementGroupsOnly = [bool]$ManagementGroupsOnly 27 | NoJsonExport = [bool]$NoJsonExport 28 | NoMDfCSecureScore = [bool]$NoMDfCSecureScore 29 | NoResourceProvidersDetailed = [bool]$NoResourceProvidersDetailed 30 | NoResourceProvidersAtAll = [bool]$NoResourceProvidersAtAll 31 | NoPolicyComplianceStates = [bool]$NoPolicyComplianceStates 32 | NoResources = [bool]$NoResources 33 | ProductVersion = $ProductVersion 34 | PolicyAtScopeOnly = [bool]$PolicyAtScopeOnly 35 | RBACAtScopeOnly = [bool]$RBACAtScopeOnly 36 | DoPSRule = [bool]$DoPSRule 37 | PSRuleFailedOnly = [bool]$PSRuleFailedOnly 38 | NoALZPolicyVersionChecker = [bool]$NoALZPolicyVersionChecker 39 | ALZPolicyAssignmentsChecker = [bool]$ALZPolicyAssignmentsChecker 40 | ALZManagementGroupsIds = $ALZManagementGroupsIds 41 | NoStorageAccountAccessAnalysis = [bool]$NoStorageAccountAccessAnalysis 42 | GitHubActionsOIDC = [bool]$GitHubActionsOIDC 43 | NoNetwork = [bool]$NoNetwork 44 | ThrottleLimit = $ThrottleLimit 45 | APIMappingCloudEnvironment = $APIMappingCloudEnvironment 46 | } 47 | Write-Host 'htParameters:' 48 | $azAPICallConf['htParameters'] | ConvertTo-Json -Depth 99 | Out-String 49 | Write-Host 'Add Azure Governance Visualizer htParameters succeeded' -ForegroundColor Green 50 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/addIndexNumberToArray.ps1: -------------------------------------------------------------------------------- 1 | function addIndexNumberToArray ( 2 | [Parameter(Mandatory = $True)] 3 | [array]$array 4 | ) { 5 | for ($i = 0; $i -lt ($array).count; $i++) { 6 | Add-Member -InputObject $array[$i] -Name '#' -Value ($i + 1) -MemberType NoteProperty 7 | } 8 | return $array 9 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/apiCallTracking.ps1: -------------------------------------------------------------------------------- 1 | function apiCallTracking { 2 | [CmdletBinding()]Param( 3 | [string]$stage, 4 | [string]$spacing 5 | ) 6 | #APITracking 7 | $APICallTrackingCount = ($azAPICallConf['arrayAPICallTracking']).Count 8 | $APICallTrackingRetriesCount = ($azAPICallConf['arrayAPICallTracking'].where({ $_.TryCounter -gt 1 } )).Count 9 | $APICallTrackingGroupedByTargetEndpoint = $azAPICallConf['arrayAPICallTracking'] | Group-Object -Property TargetEndpoint 10 | $APICallTrackingRestartDueToDuplicateNextlinkCounterCount = ($azAPICallConf['arrayAPICallTracking'].where({ $_.RestartDueToDuplicateNextlinkCounter -gt 0 } )).Count 11 | Write-Host "$($spacing)$($stage) API call stats:" 12 | $duarationStats = ($azAPICallConf['arrayAPICallTracking'].Duration | Measure-Object -Average -Maximum -Minimum) 13 | Write-Host "$($spacing) API calls total count: $APICallTrackingCount ($APICallTrackingRetriesCount retries; $APICallTrackingRestartDueToDuplicateNextlinkCounterCount nextLinkReset) | average: $($duarationStats.Average) sec, maximum: $($duarationStats.Maximum) sec, minimum: $($duarationStats.Minimum) sec" 14 | foreach ($targetEndpoint in $APICallTrackingGroupedByTargetEndpoint | Sort-Object -Property Name) { 15 | $APICallTrackingRetriesCount = ($targetEndpoint.Group.where({ $_.TryCounter -gt 1 } )).Count 16 | $APICallTrackingRestartDueToDuplicateNextlinkCounterCount = ($targetEndpoint.Group.where({ $_.RestartDueToDuplicateNextlinkCounter -gt 0 } )).Count 17 | $duarationStats = ($targetEndpoint.Group.Duration | Measure-Object -Average -Maximum -Minimum) 18 | 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" 19 | } 20 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/buildMD.ps1: -------------------------------------------------------------------------------- 1 | function buildMD { 2 | Write-Host 'Building Markdown' 3 | $startBuildMD = Get-Date 4 | $script:arrayMgs = [System.Collections.ArrayList]@() 5 | $script:arraySubs = [System.Collections.ArrayList]@() 6 | $script:arraySubsOos = [System.Collections.ArrayList]@() 7 | $markdown = $null 8 | $script:markdownhierarchyMgs = $null 9 | $script:markdownhierarchySubs = $null 10 | $script:markdownTable = $null 11 | 12 | if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { 13 | if ($azAPICallConf['htParameters'].onAzureDevOps -eq $true) { 14 | $markdown += @" 15 | # Azure Governance Visualizer - Management Group Hierarchy 16 | 17 | ## HierarchyMap (Mermaid) 18 | 19 | ::: mermaid 20 | graph $($MermaidDirection.ToUpper());`n 21 | "@ 22 | } 23 | if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { 24 | $marks = '```' 25 | $markdown += @" 26 | # Azure Governance Visualizer - Management Group Hierarchy 27 | 28 | ## HierarchyMap (Mermaid) 29 | 30 | $($marks)mermaid 31 | graph $($MermaidDirection.ToUpper());`n 32 | "@ 33 | } 34 | 35 | } 36 | else { 37 | $markdown += @" 38 | # Azure Governance Visualizer - Management Group Hierarchy 39 | 40 | $executionDateTimeInternationalReadable ($currentTimeZone) 41 | 42 | ## HierarchyMap (Mermaid) 43 | 44 | ::: mermaid 45 | graph $($MermaidDirection.ToUpper());`n 46 | "@ 47 | } 48 | 49 | processDiagramMermaid 50 | 51 | $markdown += @" 52 | $markdownhierarchyMgs 53 | $markdownhierarchySubs 54 | classDef mgr fill:#D9F0FF,stroke:#56595E,color:#000000,stroke-width:1px; 55 | classDef subs fill:#EEEEEE,stroke:#56595E,color:#000000,stroke-width:1px; 56 | "@ 57 | 58 | if (($arraySubsOos).count -gt 0) { 59 | $markdown += @' 60 | classDef subsoos fill:#FFCBC7,stroke:#56595E,color:#000000,stroke-width:1px; 61 | '@ 62 | } 63 | 64 | $markdown += @" 65 | classDef mgrprnts fill:#FFFFFF,stroke:#56595E,color:#000000,stroke-width:1px; 66 | class $(($arrayMgs | Sort-Object -Unique) -join ',') mgr; 67 | class $(($arraySubs | Sort-Object -Unique) -join ',') subs; 68 | "@ 69 | 70 | if (($arraySubsOos).count -gt 0) { 71 | $markdown += @" 72 | class $(($arraySubsOos | Sort-Object -Unique) -join ',') subsoos; 73 | "@ 74 | } 75 | 76 | if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { 77 | if ($azAPICallConf['htParameters'].onAzureDevOps -eq $true) { 78 | $markdown += @" 79 | class $mermaidprnts mgrprnts; 80 | ::: 81 | 82 | "@ 83 | } 84 | if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { 85 | ` 86 | $marks = '```' 87 | $markdown += @" 88 | class $mermaidprnts mgrprnts; 89 | $marks 90 | 91 | "@ 92 | } 93 | } 94 | else { 95 | $markdown += @" 96 | class $mermaidprnts mgrprnts; 97 | ::: 98 | 99 | "@ 100 | } 101 | 102 | $markdown += @" 103 | ## Summary 104 | `n 105 | "@ 106 | if (-not $HierarchyMapOnly) { 107 | $markdown += @" 108 | Total Management Groups: $totalMgCount (depth $mgDepth)\`n 109 | "@ 110 | 111 | if (($arraySubsOos).count -gt 0) { 112 | $markdown += @" 113 | Total Subscriptions: $totalSubIncludedAndExcludedCount ($totalSubOutOfScopeCount out-of-scope)\`n 114 | "@ 115 | } 116 | else { 117 | $markdown += @" 118 | Total Subscriptions: $totalSubIncludedAndExcludedCount\`n 119 | "@ 120 | } 121 | 122 | $markdown += @" 123 | Total Custom Policy definitions: $tenantCustomPoliciesCount\ 124 | Total Custom PolicySet definitions: $tenantCustompolicySetsCount\ 125 | Total Policy assignments: $($totalPolicyAssignmentsCount)\ 126 | Total Policy assignments ManagementGroups $($totalPolicyAssignmentsCountMg)\ 127 | Total Policy assignments Subscriptions $($totalPolicyAssignmentsCountSub)\ 128 | Total Policy assignments ResourceGroups: $($totalPolicyAssignmentsCountRg)\ 129 | Total Custom Role definitions: $totalRoleDefinitionsCustomCount\ 130 | Total Role assignments: $totalRoleAssignmentsCount\ 131 | Total Role assignments (Tenant): $totalRoleAssignmentsCountTen\ 132 | Total Role assignments (ManagementGroups): $totalRoleAssignmentsCountMG\ 133 | Total Role assignments (Subscriptions): $totalRoleAssignmentsCountSub\ 134 | Total Role assignments (ResourceGroups and Resources): $totalRoleAssignmentsResourceGroupsAndResourcesCount\ 135 | Total Blueprint definitions: $totalBlueprintDefinitionsCount\ 136 | Total Blueprint assignments: $totalBlueprintAssignmentsCount\ 137 | Total Resources: $totalResourceCount\ 138 | Total Resource Types: $totalResourceTypesCount 139 | "@ 140 | 141 | } 142 | if ($HierarchyMapOnly) { 143 | $mgsDetails = ($optimizedTableForPathQueryMg | Select-Object Level, MgId -Unique) 144 | $mgDepth = ($mgsDetails.Level | Measure-Object -Maximum).Maximum 145 | $totalMgCount = ($mgsDetails).count 146 | $totalSubCount = ($optimizedTableForPathQuerySub).count 147 | 148 | $markdown += @" 149 | Total Management Groups: $totalMgCount (depth $mgDepth)\ 150 | Total Subscriptions: $totalSubCount 151 | "@ 152 | 153 | } 154 | 155 | $markdown += @" 156 | `n 157 | ## Hierarchy Table 158 | 159 | | **MgLevel** | **MgName** | **MgId** | **MgParentName** | **MgParentId** | **SubName** | **SubId** | 160 | |-------------|-------------|-------------|-------------|-------------|-------------|-------------| 161 | $markdownTable 162 | "@ 163 | 164 | $markdown | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).md" -Encoding utf8 -Force 165 | $endBuildMD = Get-Date 166 | Write-Host "Building Markdown total duration: $((New-TimeSpan -Start $startBuildMD -End $endBuildMD).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildMD -End $endBuildMD).TotalSeconds) seconds)" 167 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/buildPolicyAllJSON.ps1: -------------------------------------------------------------------------------- 1 | function buildPolicyAllJSON { 2 | Write-Host 'Creating PolicyAll JSON' 3 | $startPolicyAllJSON = Get-Date 4 | $htPolicyAndPolicySet = [ordered]@{} 5 | $htPolicyAndPolicySet.Policy = [ordered]@{} 6 | $htPolicyAndPolicySet.PolicySet = [ordered]@{} 7 | $htPolicyAndPolicySet.PolicyAssignment = [ordered]@{} 8 | foreach ($policy in ($tenantPoliciesDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicyDefinitionId)) { 9 | $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()) = [ordered]@{ 10 | PolicyType = $policy.Type 11 | ScopeMGLevel = $policy.ScopeMGLevel 12 | Scope = $policy.Scope 13 | ScopeId = $policy.scopeId 14 | PolicyDisplayName = $policy.PolicyDisplayName 15 | PolicyDefinitionName = $policy.PolicyDefinitionName 16 | PolicyDefinitionId = $policy.PolicyDefinitionId 17 | PolicyEffect = $policy.PolicyEffect 18 | PolicyCategory = $policy.PolicyCategory 19 | UniqueAssignmentsCount = $policy.UniqueAssignmentsCount 20 | UniqueAssignments = $policy.UniqueAssignments 21 | UsedInPolicySetsCount = $policy.UsedInPolicySetsCount 22 | UsedInPolicySets = $policy.UsedInPolicySet4JSON 23 | CreatedOn = $policy.CreatedOn 24 | CreatedBy = $policy.CreatedByJson 25 | UpdatedOn = $policy.UpdatedOn 26 | UpdatedBy = $policy.UpdatedByJson 27 | JSON = $policy.Json 28 | } 29 | } 30 | foreach ($policySet in ($tenantPolicySetsDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicySetDefinitionId)) { 31 | $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()) = [ordered]@{ 32 | PolicySetType = $policySet.Type 33 | ScopeMGLevel = $policySet.ScopeMGLevel 34 | Scope = $policySet.Scope 35 | ScopeId = $policySet.scopeId 36 | PolicySetDisplayName = $policySet.PolicySetDisplayName 37 | PolicySetDefinitionName = $policySet.PolicySetDefinitionName 38 | PolicySetDefinitionId = $policySet.PolicySetDefinitionId 39 | PolicySetCategory = $policySet.PolicySetCategory 40 | UniqueAssignmentsCount = $policySet.UniqueAssignmentsCount 41 | UniqueAssignments = $policySet.UniqueAssignments 42 | PoliciesUsedCount = $policySet.PoliciesUsedCount 43 | PoliciesUsed = $policySet.PoliciesUsed4JSON 44 | CreatedOn = $policySet.CreatedOn 45 | CreatedBy = $policySet.CreatedByJson 46 | UpdatedOn = $policySet.UpdatedOn 47 | UpdatedBy = $policySet.UpdatedByJson 48 | JSON = $policySet.Json 49 | } 50 | } 51 | foreach ($key in $htCacheAssignmentsPolicy.keys | Sort-Object) { 52 | $htPolicyAndPolicySet.PolicyAssignment.($key.ToLower()) = $htCacheAssignmentsPolicy.($key).Assignment 53 | } 54 | Write-Host " Exporting PolicyAll JSON '$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json'" 55 | $htPolicyAndPolicySet | ConvertTo-Json -Depth 99 | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json" -Encoding utf8 -Force 56 | 57 | $endPolicyAllJSON = Get-Date 58 | Write-Host "Creating PolicyAll JSON duration: $((New-TimeSpan -Start $startPolicyAllJSON -End $endPolicyAllJSON).TotalSeconds) seconds" 59 | } 60 | -------------------------------------------------------------------------------- /pwsh/dev/functions/checkAzGovVizVersion.ps1: -------------------------------------------------------------------------------- 1 | function checkAzGovVizVersion { 2 | try { 3 | $getRepoVersion = Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/master/version.json' 4 | $repoVersion = ($getRepoVersion.Content | ConvertFrom-Json).ProductVersion 5 | 6 | $script:azGovVizNewerVersionAvailable = $false 7 | if ($repoVersion -ne $ProductVersion) { 8 | $repoVersionSplit = $repoVersion -split '\.' 9 | $repoVersionMajor = $repoVersionSplit[0] 10 | $repoVersionMinor = $repoVersionSplit[1] 11 | $repoVersionPatch = $repoVersionSplit[2] 12 | 13 | $ProductVersionSplit = $ProductVersion -split '\.' 14 | $ProductVersionMajor = $ProductVersionSplit[0] 15 | $ProductVersionMinor = $ProductVersionSplit[1] 16 | $ProductVersionPatch = $ProductVersionSplit[2] 17 | 18 | if ($repoVersionMajor -ne $ProductVersionMajor) { 19 | $versionDrift = 'major' 20 | } 21 | elseif ($repoVersionMinor -ne $ProductVersionMinor) { 22 | $versionDrift = 'minor' 23 | } 24 | elseif ($repoVersionPatch -ne $ProductVersionPatch) { 25 | $versionDrift = 'patch' 26 | } 27 | else { 28 | $versionDrift = 'unknown' 29 | } 30 | 31 | $versionDriftSummary = "$repoVersion ($versionDrift)" 32 | $script:azGovVizVersionOnRepositoryFull = $versionDriftSummary 33 | $script:azGovVizNewerVersionAvailable = $true 34 | $script:azGovVizNewerVersionAvailableHTML = 'Get the latest Azure Governance Visualizer version ' + $azGovVizVersionOnRepositoryFull + '! ' 35 | } 36 | else { 37 | Write-Host "Azure Governance Visualizer version is up to date '$ProductVersion'" -ForegroundColor Green 38 | } 39 | } 40 | catch { 41 | #skip 42 | Write-Host 'Azure Governance Visualizer version check skipped' -ForegroundColor Magenta 43 | } 44 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/createTagList.ps1: -------------------------------------------------------------------------------- 1 | function createTagList { 2 | $startTagListArray = Get-Date 3 | Write-Host 'Creating TagList array' 4 | 5 | $tagsSubRgResCount = ($htAllTagList.'AllScopes'.Keys).Count 6 | $tagsSubsriptionCount = ($htAllTagList.'Subscription'.Keys).Count 7 | $tagsResourceGroupCount = ($htAllTagList.'ResourceGroup'.Keys).Count 8 | $tagsResourceCount = ($htAllTagList.'Resource'.Keys).Count 9 | Write-Host " Total Number of ALL unique Tag Names: $tagsSubRgResCount" 10 | Write-Host " Total Number of Subscription unique Tag Names: $tagsSubsriptionCount" 11 | Write-Host " Total Number of ResourceGroup unique Tag Names: $tagsResourceGroupCount" 12 | Write-Host " Total Number of Resource unique Tag Names: $tagsResourceCount" 13 | 14 | foreach ($tagScope in $htAllTagList.keys) { 15 | foreach ($tagScopeTagName in $htAllTagList.($tagScope).keys) { 16 | $null = $script:arrayTagList.Add([PSCustomObject]@{ 17 | Scope = $tagScope 18 | TagName = ($tagScopeTagName) 19 | TagCount = $htAllTagList.($tagScope).($tagScopeTagName) 20 | }) 21 | } 22 | } 23 | $endTagListArray = Get-Date 24 | Write-Host "Creating TagList array duration: $((New-TimeSpan -Start $startTagListArray -End $endTagListArray).TotalMinutes) minutes ($((New-TimeSpan -Start $startTagListArray -End $endTagListArray).TotalSeconds) seconds)" 25 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/detectPolicyEffect.ps1: -------------------------------------------------------------------------------- 1 | function detectPolicyEffect { 2 | [CmdletBinding()] 3 | Param 4 | ( 5 | [object] 6 | $policyDefinition 7 | ) 8 | 9 | $htEffect = @{ 10 | defaultValue = 'n/a' 11 | allowedValues = 'n/a' 12 | fixedValue = 'n/a' 13 | } 14 | if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.policyRule.then.effect)) { 15 | if ($policyDefinition.properties.policyRule.then.effect -in $ValidPolicyEffects) { 16 | # $arrayeffect += "fixed: $($policyDefinition.properties.policyRule.then.effect)" 17 | # return $arrayeffect 18 | $htEffect.fixedValue = $policyDefinition.properties.policyRule.then.effect 19 | return $htEffect 20 | } 21 | else { 22 | $Regex = [Regex]::new("(?<=\[parameters\(')(.*)(?='\)\])") 23 | $Match = $Regex.Match($policyDefinition.properties.policyRule.then.effect) 24 | if ($Match.Success) { 25 | if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value))) { 26 | 27 | #defaultValue 28 | if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'defaultvalue') { 29 | if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).defaultValue)) { 30 | if ($policyDefinition.properties.parameters.($Match.Value).defaultValue -in $ValidPolicyEffects) { 31 | #$arrayeffect += "default: $($policyDefinition.properties.parameters.($Match.Value).defaultValue)" 32 | $htEffect.defaultValue = $policyDefinition.properties.parameters.($Match.Value).defaultValue 33 | } 34 | else { 35 | Write-Host "invalid defaultValue effect $($policyDefinition.properties.parameters.($Match.Value).defaultValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 36 | } 37 | } 38 | else { 39 | Write-Host "defaultValue empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 40 | } 41 | } 42 | else { 43 | Write-Host "finding: Policy has no defaultvalue for effect: $($policyDefinition.id) ($($policyDefinition.properties.policyType))" 44 | } 45 | #allowedValues 46 | if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'allowedValues') { 47 | if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).allowedValues)) { 48 | if ($policyDefinition.properties.parameters.($Match.Value).allowedValues.Count -gt 0) { 49 | #Write-Host "allowedValues count $($policyDefinition.properties.parameters.($Match.Value).allowedValues) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 50 | $arrayAllowed = [System.Collections.ArrayList]@() 51 | foreach ($allowedValue in $policyDefinition.properties.parameters.($Match.Value).allowedValues) { 52 | if ($allowedValue -in $ValidPolicyEffects) { 53 | $null = $arrayAllowed.Add($allowedValue) 54 | } 55 | else { 56 | Write-Host "invalid allowedValue effect $($allowedValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 57 | } 58 | } 59 | #$arrayeffect += "allowed: $(($arrayAllowed | Sort-Object) -join ', ')" 60 | $htEffect.allowedValues = ($arrayAllowed | Sort-Object) -join ',' 61 | } 62 | } 63 | else { 64 | Write-Host "allowedValues empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 65 | } 66 | } 67 | else { 68 | Write-Host "no allowedValues- $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 69 | } 70 | 71 | } 72 | else { 73 | Write-Host "unexpected - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 74 | } 75 | 76 | return $htEffect 77 | } 78 | } 79 | } 80 | else { 81 | Write-Host "no then effect - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" 82 | } 83 | return $htEffect 84 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/exportBaseCSV.ps1: -------------------------------------------------------------------------------- 1 | function exportBaseCSV { 2 | if (-not $NoCsvExport) { 3 | Write-Host "Exporting CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName).csv'" 4 | $startBuildCSV = Get-Date 5 | 6 | $outprops = $newtable[0].PSObject.Properties.Name 7 | if (-not $HierarchyMapOnly -and -not $HierarchyMapOnlyCustomDataJSON) { 8 | $outprops.Set($outprops.IndexOf('PolicyAssignmentNotScopes'), @{L = 'PolicyAssignmentNotScopes'; E = { ($_.PolicyAssignmentNotScopes -join "$CsvDelimiterOpposite ") } }) 9 | } 10 | if ($CsvExportUseQuotesAsNeeded) { 11 | $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 12 | } 13 | else { 14 | $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 15 | } 16 | 17 | $endBuildCSV = Get-Date 18 | Write-Host "Exporting CSV total duration: $((New-TimeSpan -Start $startBuildCSV -End $endBuildCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildCSV -End $endBuildCSV).TotalSeconds) seconds)" 19 | } 20 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/exportResourceLocks.ps1: -------------------------------------------------------------------------------- 1 | function exportResourceLocks { 2 | $arrayResourceLocks4CSV = [System.Collections.ArrayList]@() 3 | foreach ($sub in $htResourceLocks.Keys) { 4 | $hlper = $htSubscriptionsMgPath.($sub) 5 | $subscriptionDisplayName = $hlper.DisplayName 6 | $mgPath = $hlper.ParentNameChainDelimited 7 | #sub 8 | if ($htResourceLocks.($sub).SubscriptionLocksCannotDeleteCount -eq 1) { 9 | $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ 10 | SubscriptionId = $sub 11 | SubscriptionName = $subscriptionDisplayName 12 | MGPath = $mgPath 13 | ScopeType = 'Subscription' 14 | Lock = 'CannotDelete' 15 | Id = "/subscriptions/$sub" 16 | ResourceType = 'Microsoft.Resources/subscriptions' 17 | }) 18 | } 19 | if ($htResourceLocks.($sub).SubscriptionLocksReadOnlyCount -eq 1) { 20 | $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ 21 | SubscriptionId = $sub 22 | SubscriptionName = $subscriptionDisplayName 23 | MGPath = $mgPath 24 | ScopeType = 'Subscription' 25 | Lock = 'ReadOnly' 26 | Id = "/subscriptions/$sub" 27 | ResourceType = 'Microsoft.Resources/subscriptions' 28 | }) 29 | } 30 | #rg 31 | if ($htResourceLocks.($sub).ResourceGroupsLocksCannotDeleteCount -gt 0) { 32 | foreach ($res in $htResourceLocks.($sub).ResourceGroupsLocksCannotDelete) { 33 | $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ 34 | SubscriptionId = $sub 35 | SubscriptionName = $subscriptionDisplayName 36 | MGPath = $mgPath 37 | ScopeType = 'ResourceGroup' 38 | Lock = 'CannotDelete' 39 | Id = $res.rg 40 | ResourceType = 'Microsoft.Resources/subscriptions/resourceGroups' 41 | }) 42 | } 43 | } 44 | if ($htResourceLocks.($sub).ResourceGroupsLocksReadOnlyCount -gt 0) { 45 | foreach ($res in $htResourceLocks.($sub).ResourceGroupsLocksReadOnly) { 46 | $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ 47 | SubscriptionId = $sub 48 | SubscriptionName = $subscriptionDisplayName 49 | MGPath = $mgPath 50 | ScopeType = 'ResourceGroup' 51 | Lock = 'ReadOnly' 52 | Id = $res.rg 53 | ResourceType = 'Microsoft.Resources/subscriptions/resourceGroups' 54 | }) 55 | } 56 | } 57 | #res 58 | if ($htResourceLocks.($sub).ResourcesLocksCannotDeleteCount -gt 0) { 59 | foreach ($res in $htResourceLocks.($sub).ResourcesLocksCannotDelete) { 60 | $resSplit = ($res.res -split '/') 61 | $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ 62 | SubscriptionId = $sub 63 | SubscriptionName = $subscriptionDisplayName 64 | MGPath = $mgPath 65 | ScopeType = 'Resource' 66 | Lock = 'CannotDelete' 67 | Id = $res.res 68 | ResourceType = "$($resSplit[6])/$($resSplit[7])" 69 | }) 70 | } 71 | } 72 | if ($htResourceLocks.($sub).ResourcesLocksReadOnlyCount -gt 0) { 73 | foreach ($res in $htResourceLocks.($sub).ResourcesLocksReadOnly) { 74 | $resSplit = ($res.res -split '/') 75 | $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ 76 | SubscriptionId = $sub 77 | SubscriptionName = $subscriptionDisplayName 78 | MGPath = $mgPath 79 | ScopeType = 'Resource' 80 | Lock = 'ReadOnly' 81 | Id = $res.res 82 | ResourceType = "$($resSplit[6])/$($resSplit[7])" 83 | }) 84 | } 85 | } 86 | } 87 | if ($arrayResourceLocks4CSV.count -gt 0) { 88 | if (-not $NoCsvExport) { 89 | Write-Host "Exporting ResourceLocks CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceLocks.csv'" 90 | $arrayResourceLocks4CSV | Sort-Object -Property ScopeType, Lock, SubscriptionId, Id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceLocks.csv" -Delimiter "$csvDelimiter" -NoTypeInformation 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getDefaultManagementGroup.ps1: -------------------------------------------------------------------------------- 1 | function getDefaultManagementGroup { 2 | $currentTask = 'Get Default Management Group' 3 | Write-Host $currentTask 4 | #https://learn.microsoft.com/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group 5 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2023-04-01" 6 | $method = 'GET' 7 | #fix https://github.com/Azure/Azure-Governance-Visualizer/issues/53 8 | $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -skipOnErrorCode 403 9 | 10 | if ($settingsMG) { 11 | if (($settingsMG).count -gt 0) { 12 | Write-Host " default ManagementGroup Id: $($settingsMG.properties.defaultManagementGroup)" 13 | $script:defaultManagementGroupId = $settingsMG.properties.defaultManagementGroup 14 | Write-Host " requireAuthorizationForGroupCreation: $($settingsMG.properties.requireAuthorizationForGroupCreation)" 15 | $script:requireAuthorizationForGroupCreation = $settingsMG.properties.requireAuthorizationForGroupCreation 16 | } 17 | else { 18 | Write-Host " default ManagementGroup: $(($azAPICallConf['checkContext']).Tenant.Id) (Tenant Root)" 19 | $script:defaultManagementGroupId = ($azAPICallConf['checkContext']).Tenant.Id 20 | $script:requireAuthorizationForGroupCreation = $false 21 | } 22 | } 23 | else { 24 | Write-Host " default ManagementGroup: could not be determined, flagging default ManagementGroup as 'unknown.'" 25 | $script:defaultManagementGroupId = 'unknown.' 26 | $script:requireAuthorizationForGroupCreation = 'unknown.' 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getEntities.ps1: -------------------------------------------------------------------------------- 1 | function getEntities { 2 | Write-Host 'Entities' 3 | $startEntities = Get-Date 4 | $currentTask = ' Getting Entities' 5 | Write-Host $currentTask 6 | #https://management.azure.com/providers/Microsoft.Management/getEntities?api-version=2020-02-01 7 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/getEntities?api-version=2020-02-01" 8 | $method = 'POST' 9 | $arrayEntitiesFromAPIInitial = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask 10 | Write-Host " $($arrayEntitiesFromAPIInitial.Count) Entities returned" 11 | 12 | $script:arrayEntitiesFromAPI = [System.Collections.ArrayList]@() 13 | $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions = @{} 14 | foreach ($entry in $arrayEntitiesFromAPIInitial) { 15 | if ($entry.Type -eq '/subscriptions') { 16 | if ($htSubscriptionsFromOtherTenants.($entry.name)) { 17 | $subdetail = $htSubscriptionsFromOtherTenants.($entry.name).subdetails 18 | Write-Host " Excluded Subscription '$($subDetail.displayName)' ($($entry.name)) (foreign tenantId: '$($subDetail.tenantId)')" -ForegroundColor DarkRed 19 | continue 20 | } 21 | if (-not $htAllSubscriptionsFromAPI.($entry.name)) { 22 | #not contained in subscriptions 23 | $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($entry.name) = $entry 24 | Write-Host " Excluded Subscription '$($entry.properties.displayName)' ($($entry.name)) (contained in GetEntities, not contained in GetSubscriptions)" -ForegroundColor DarkRed 25 | continue 26 | } 27 | #test 28 | # if ($entry.name -eq '') { 29 | # $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($entry.name) = $entry 30 | # Write-Host " Excluded Subscription '$($entry.properties.displayName)' ($($entry.name)) (contained in GetEntities, not contained in GetSubscriptions)" -ForegroundColor DarkRed 31 | # continue 32 | # } 33 | } 34 | 35 | $null = $script:arrayEntitiesFromAPI.Add($entry) 36 | } 37 | 38 | Write-Host " $($arrayEntitiesFromAPI.Count)/$($arrayEntitiesFromAPIInitial.Count) Entities relevant" 39 | 40 | $endEntities = Get-Date 41 | Write-Host " Getting Entities duration: $((New-TimeSpan -Start $startEntities -End $endEntities).TotalSeconds) seconds" 42 | 43 | $startEntitiesdata = Get-Date 44 | Write-Host ' Processing Entities data' 45 | $script:htSubscriptionsMgPath = @{} 46 | $script:htManagementGroupsMgPath = @{} 47 | $script:htEntities = @{} 48 | $script:htEntitiesPlain = @{} 49 | 50 | foreach ($entity in $arrayEntitiesFromAPI) { 51 | $script:htEntitiesPlain.($entity.Name) = @{} 52 | $script:htEntitiesPlain.($entity.Name) = $entity 53 | } 54 | 55 | foreach ($entity in $arrayEntitiesFromAPI) { 56 | if ($entity.Type -eq '/subscriptions') { 57 | $parent = $entity.properties.parent.Id -replace '.*/' 58 | $parentId = $entity.properties.parent.Id 59 | 60 | $array = $entity.properties.parentNameChain 61 | $array += $entity.name 62 | 63 | $script:htSubscriptionsMgPath.($entity.name) = @{ 64 | ParentNameChain = $entity.properties.parentNameChain 65 | ParentNameChainDelimited = $entity.properties.parentNameChain -join '/' 66 | Parent = $parent 67 | ParentName = $htEntitiesPlain.($parent).properties.displayName 68 | DisplayName = $entity.properties.displayName 69 | path = $array 70 | pathDelimited = $array -join '/' 71 | level = (($entity.properties.parentNameChain).Count - 1) 72 | } 73 | 74 | } 75 | if ($entity.Type -eq 'Microsoft.Management/managementGroups') { 76 | if ([string]::IsNullOrEmpty($entity.properties.parent.Id)) { 77 | $parent = '__TenantRoot__' 78 | $parentId = '__TenantRoot__' 79 | } 80 | else { 81 | $parent = $entity.properties.parent.Id -replace '.*/' 82 | $parentId = $entity.properties.parent.Id 83 | } 84 | 85 | $array = $entity.properties.parentNameChain 86 | $array += $entity.name 87 | $script:htManagementGroupsMgPath.($entity.name) = @{ 88 | ParentNameChain = $entity.properties.parentNameChain 89 | ParentNameChainDelimited = $entity.properties.parentNameChain -join '/' 90 | ParentNameChainCount = ($entity.properties.parentNameChain | Measure-Object).Count 91 | Parent = $parent 92 | ChildMgsAll = ($arrayEntitiesFromAPI.where( { $_.Type -eq 'Microsoft.Management/managementGroups' -and $_.properties.ParentNameChain -contains $entity.name } )).Name 93 | ChildMgsDirect = ($arrayEntitiesFromAPI.where( { $_.Type -eq 'Microsoft.Management/managementGroups' -and $_.properties.Parent.Id -replace '.*/' -eq $entity.name } )).Name 94 | DisplayName = $entity.properties.displayName 95 | Id = ($entity.name) 96 | path = $array 97 | pathDelimited = $array -join '/' 98 | level = $array.Count 99 | } 100 | 101 | } 102 | 103 | $script:htEntities.($entity.name) = @{ 104 | ParentNameChain = $entity.properties.parentNameChain 105 | Parent = $parent 106 | ParentId = $parentId 107 | } 108 | 109 | 110 | if ($parent -eq '__TenantRoot__') { 111 | $parentDisplayName = '__TenantRoot__' 112 | } 113 | else { 114 | $parentDisplayName = $htEntitiesPlain.($htEntities.($entity.name).Parent).properties.displayName 115 | } 116 | $script:htEntities.($entity.name) = @{ 117 | ParentNameChain = $entity.properties.parentNameChain 118 | Parent = $parent 119 | ParentId = $parentId 120 | ParentDisplayName = $parentDisplayName 121 | DisplayName = $entity.properties.displayName 122 | Id = $entity.Name 123 | Type = $entity.Type 124 | } 125 | 126 | } 127 | 128 | Write-Host " $(($htManagementGroupsMgPath.Keys).Count) relevant Management Groups" 129 | Write-Host " $(($htSubscriptionsMgPath.Keys).Count) relevant Subscriptions" 130 | 131 | $endEntitiesdata = Get-Date 132 | Write-Host " Processing Entities data duration: $((New-TimeSpan -Start $startEntitiesdata -End $endEntitiesdata).TotalSeconds) seconds" 133 | 134 | $script:arrayEntitiesFromAPISubscriptionsCount = ($arrayEntitiesFromAPI.where( { $_.type -eq '/subscriptions' -and $_.properties.parentNameChain -contains $ManagementGroupId } ) | Sort-Object -Property id -Unique).count 135 | $script:arrayEntitiesFromAPIManagementGroupsCount = ($arrayEntitiesFromAPI.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.properties.parentNameChain -contains $ManagementGroupId } ) | Sort-Object -Property id -Unique).count + 1 136 | 137 | $endEntities = Get-Date 138 | Write-Host "Processing Entities duration: $((New-TimeSpan -Start $startEntities -End $endEntities).TotalSeconds) seconds" 139 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getFileNaming.ps1: -------------------------------------------------------------------------------- 1 | function getFileNaming { 2 | if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { 3 | if ($HierarchyMapOnly) { 4 | $script:fileName = "AzGovViz_HierarchyMapOnly_$($ManagementGroupId)" 5 | } 6 | elseif ($azAPICallConf['htParameters'].ManagementGroupsOnly -eq $true) { 7 | $script:fileName = "AzGovViz_ManagementGroupsOnly_$($ManagementGroupId)" 8 | } 9 | else { 10 | $script:fileName = "AzGovViz_$($ManagementGroupId)" 11 | } 12 | } 13 | else { 14 | if ($HierarchyMapOnly) { 15 | $script:fileName = "AzGovViz_HierarchyMapOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" 16 | } 17 | elseif ($azAPICallConf['htParameters'].ManagementGroupsOnly -eq $true) { 18 | $script:fileName = "AzGovViz_ManagementGroupsOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" 19 | } 20 | else { 21 | $script:fileName = "AzGovViz_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getGroupmembers.ps1: -------------------------------------------------------------------------------- 1 |  2 | function getGroupmembers($aadGroupId, $aadGroupDisplayName) { 3 | if (-not $htAADGroupsDetails.($aadGroupId)) { 4 | $script:htAADGroupsDetails.$aadGroupId = @{ 5 | Id = $aadGroupId 6 | displayname = $aadGroupDisplayName 7 | } 8 | 9 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupId)/transitiveMembers" 10 | $method = 'GET' 11 | $aadGroupMembers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupmembers $($aadGroupId)" 12 | 13 | if ($aadGroupMembers -eq 'Request_ResourceNotFound') { 14 | $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ 15 | groupId = $aadGroupId 16 | }) 17 | } 18 | 19 | $aadGroupMembersAll = ($aadGroupMembers) 20 | $aadGroupMembersUsers = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.user' } ) 21 | $aadGroupMembersGroups = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.group' } ) 22 | $aadGroupMembersServicePrincipals = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.servicePrincipal' } ) 23 | 24 | $aadGroupMembersAllCount = $aadGroupMembersAll.count 25 | $aadGroupMembersUsersCount = $aadGroupMembersUsers.count 26 | $aadGroupMembersGroupsCount = $aadGroupMembersGroups.count 27 | $aadGroupMembersServicePrincipalsCount = $aadGroupMembersServicePrincipals.count 28 | #for SP stuff 29 | if ($aadGroupMembersServicePrincipalsCount -gt 0) { 30 | foreach ($identity in $aadGroupMembersServicePrincipals) { 31 | $arrayIdentityObject = [System.Collections.ArrayList]@() 32 | if ($identity.servicePrincipalType -eq 'Application') { 33 | if ($identity.appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Tenant.Id) { 34 | $null = $arrayIdentityObject.Add([PSCustomObject]@{ 35 | type = 'ServicePrincipal' 36 | spTypeConcatinated = 'SP APP INT' 37 | servicePrincipalType = $identity.servicePrincipalType 38 | id = $identity.id 39 | appid = $identity.appId 40 | displayName = $identity.displayName 41 | appOwnerOrganizationId = $identity.appOwnerOrganizationId 42 | alternativeNames = $identity.alternativeNames 43 | }) 44 | } 45 | else { 46 | $null = $arrayIdentityObject.Add([PSCustomObject]@{ 47 | type = 'ServicePrincipal' 48 | spTypeConcatinated = 'SP APP EXT' 49 | servicePrincipalType = $identity.servicePrincipalType 50 | id = $identity.id 51 | appid = $identity.appId 52 | displayName = $identity.displayName 53 | appOwnerOrganizationId = $identity.appOwnerOrganizationId 54 | alternativeNames = $identity.alternativeNames 55 | }) 56 | } 57 | } 58 | elseif ($identity.servicePrincipalType -eq 'ManagedIdentity') { 59 | $miType = 'unknown' 60 | if ($identity.alternativeNames) { 61 | foreach ($altName in $identity.alternativeNames) { 62 | if ($altName -like 'isExplicit=*') { 63 | $splitAltName = $altName.split('=') 64 | if ($splitAltName[1] -eq 'true') { 65 | $miType = 'Usr' 66 | } 67 | if ($splitAltName[1] -eq 'false') { 68 | $miType = 'Sys' 69 | } 70 | } 71 | } 72 | } 73 | $null = $arrayIdentityObject.Add([PSCustomObject]@{ 74 | type = 'ServicePrincipal' 75 | spTypeConcatinated = "SP MI $miType" 76 | servicePrincipalType = $identity.servicePrincipalType 77 | id = $identity.id 78 | appid = $identity.appId 79 | displayName = $identity.displayName 80 | appOwnerOrganizationId = $identity.appOwnerOrganizationId 81 | alternativeNames = $identity.alternativeNames 82 | }) 83 | } 84 | else { 85 | $null = $arrayIdentityObject.Add([PSCustomObject]@{ 86 | type = 'servicePrincipal' 87 | spTypeConcatinated = "SP $($identity.servicePrincipalType)" 88 | servicePrincipalType = $identity.servicePrincipalType 89 | id = $identity.id 90 | appid = $identity.appId 91 | displayName = $identity.displayName 92 | appOwnerOrganizationId = $identity.appOwnerOrganizationId 93 | alternativeNames = $identity.alternativeNames 94 | }) 95 | } 96 | if (-not $htServicePrincipals.($identity.id)) { 97 | #Write-Host "$($identity.displayName) $($identity.id) added - - - - - - - - " 98 | #$script:htServicePrincipals.($identity.id) = @{} 99 | $script:htServicePrincipals.($identity.id) = $arrayIdentityObject 100 | } 101 | } 102 | } 103 | 104 | #guests 105 | if ($aadGroupMembersUsersCount -gt 0) { 106 | $cntx = 0 107 | $cnty = 0 108 | foreach ($aadGroupMembersUser in $aadGroupMembersUsers | Sort-Object -Property id -Unique) { 109 | $cntx++ 110 | if ($aadGroupMembersUser.userType -eq 'Guest') { 111 | if (-not $htUserTypesGuest.($aadGroupMembersUser.id)) { 112 | $cnty++ 113 | #Write-Host "$($aadGroupMembersUser.id) is Guest" 114 | $script:htUserTypesGuest.($aadGroupMembersUser.id) = @{ 115 | userType = 'Guest' 116 | } 117 | } 118 | else { 119 | #Write-Host "$($aadGroupMembersUser.id) already known as Guest" 120 | } 121 | } 122 | } 123 | } 124 | 125 | $script:htAADGroupsDetails.($aadGroupId).MembersAllCount = $aadGroupMembersAllCount 126 | $script:htAADGroupsDetails.($aadGroupId).MembersUsersCount = $aadGroupMembersUsersCount 127 | $script:htAADGroupsDetails.($aadGroupId).MembersGroupsCount = $aadGroupMembersGroupsCount 128 | $script:htAADGroupsDetails.($aadGroupId).MembersServicePrincipalsCount = $aadGroupMembersServicePrincipalsCount 129 | 130 | if ($aadGroupMembersAllCount -gt 0) { 131 | $script:htAADGroupsDetails.($aadGroupId).MembersAll = $aadGroupMembersAll 132 | 133 | if ($aadGroupMembersUsersCount -gt 0) { 134 | $script:htAADGroupsDetails.($aadGroupId).MembersUsers = $aadGroupMembersUsers 135 | } 136 | if ($aadGroupMembersGroupsCount -gt 0) { 137 | $script:htAADGroupsDetails.($aadGroupId).MembersGroups = $aadGroupMembersGroups 138 | } 139 | if ($aadGroupMembersServicePrincipalsCount -gt 0) { 140 | $script:htAADGroupsDetails.($aadGroupId).MembersServicePrincipals = $aadGroupMembersServicePrincipals 141 | } 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getMDfCSecureScoreMG.ps1: -------------------------------------------------------------------------------- 1 | function getMDfCSecureScoreMG { 2 | $start = Get-Date 3 | $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' 4 | Write-Host $currentTask 5 | #ref: https://learn.microsoft.com/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group 6 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01" 7 | $method = 'POST' 8 | 9 | $query = @' 10 | SecurityResources 11 | | where type == 'microsoft.security/securescores' 12 | | project subscriptionId, 13 | subscriptionTotal = iff(properties.score.max == 0, 0.00, round(tolong(properties.weight) * todouble(properties.score.current)/tolong(properties.score.max),2)), 14 | weight = tolong(iff(properties.weight == 0, 1, properties.weight)) 15 | | join kind=leftouter ( 16 | ResourceContainers 17 | | where type == 'microsoft.resources/subscriptions' and properties.state == 'Enabled' 18 | | project subscriptionId, mgChain=properties.managementGroupAncestorsChain ) 19 | on subscriptionId 20 | | mv-expand mg=mgChain 21 | | summarize sumSubs = sum(subscriptionTotal), sumWeight = sum(weight), resultsNum = count() by tostring(mg.displayName), mgId = tostring(mg.name) 22 | | extend secureScore = iff(tolong(resultsNum) == 0, 404.00, round(sumSubs/sumWeight*100,2)) 23 | | project mgDisplayName=mg_displayName, mgId, sumSubs, sumWeight, resultsNum, secureScore 24 | | order by mgDisplayName asc 25 | '@ 26 | 27 | $body = @" 28 | { 29 | "query": "$($query)", 30 | "managementGroups":[ 31 | "$($ManagementGroupId)" 32 | ] 33 | } 34 | "@ 35 | 36 | $getMgAscSecureScore = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' -unhandledErrorAction ContinueQuiet 37 | if ($getMgAscSecureScore) { 38 | Write-Host " Retrieved 'Microsoft Defender for Cloud' SecureScore for $($getMgAscSecureScore.Count) Management Groups" 39 | foreach ($entry in $getMgAscSecureScore) { 40 | $script:htMgASCSecureScore.($entry.mgId) = @{} 41 | if ($entry.secureScore -eq 404) { 42 | $script:htMgASCSecureScore.($entry.mgId).SecureScore = 'n/a' 43 | } 44 | else { 45 | $script:htMgASCSecureScore.($entry.mgId).SecureScore = $entry.secureScore 46 | } 47 | } 48 | } 49 | else { 50 | Write-Host ' Microsoft Defender for Cloud SecureScore for Management Groups will not be available' -ForegroundColor Yellow 51 | } 52 | 53 | $end = Get-Date 54 | 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)" 55 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getPolicyHash.ps1: -------------------------------------------------------------------------------- 1 | function getPolicyHash { 2 | param ( 3 | [Parameter(Mandatory)] 4 | [string] 5 | $json 6 | ) 7 | return [string]([System.BitConverter]::ToString([System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($json)))) 8 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getPolicyRemediation.ps1: -------------------------------------------------------------------------------- 1 | function getPolicyRemediation { 2 | $currentTask = 'Getting NonCompliant (dine/modify)' 3 | Write-Host $currentTask 4 | #ref: https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph/resources/resources 5 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01" 6 | $method = 'POST' 7 | 8 | if ($ManagementGroupsOnly) { 9 | $queryNonCompliant = @' 10 | policyresources 11 | | 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' 12 | | 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) 13 | | sort by count_, assignmentId, definitionId, policyDefinitionReferenceId, effect 14 | '@ 15 | } 16 | else { 17 | $queryNonCompliant = @' 18 | policyresources 19 | | where (properties.policyDefinitionAction =~ 'deployifnotexists' or properties.policyDefinitionAction =~ 'modify') and properties.complianceState =~ 'NonCompliant' 20 | | 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) 21 | | sort by count_, assignmentId, definitionId, policyDefinitionReferenceId, effect 22 | '@ 23 | } 24 | 25 | 26 | $body = @" 27 | { 28 | "query": "$($queryNonCompliant)", 29 | "managementGroups":[ 30 | "$($ManagementGroupId)" 31 | ], 32 | "options": { 33 | "`$top": 1000 34 | } 35 | } 36 | "@ 37 | 38 | $getNonCompliant = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' 39 | $script:arrayRemediatable = [System.Collections.ArrayList]@() 40 | Write-Host " Found $($getNonCompliant.Count) remediatable Policy definitions" 41 | if ($getNonCompliant.Count -gt 0) { 42 | Write-Host ' Enriching remediatable assignments with displayNames' 43 | foreach ($nonCompliant in $getNonCompliant) { 44 | 45 | if ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower())) { 46 | if ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { 47 | $policyAssignmentPolicyOrPolicySet = 'policySetDefinition' 48 | $policySetDefinitionId = $htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId 49 | $policySetDefinitionDisplayName = $htCacheDefinitionsPolicySet.($policySetDefinitionId.ToLower()).DisplayName 50 | $policySetDefinitionName = $policySetDefinitionId -replace '.*/' 51 | $policySetDefinitionType = $htCacheDefinitionsPolicySet.($policySetDefinitionId.ToLower()).Type 52 | } 53 | elseif ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { 54 | $policyAssignmentPolicyOrPolicySet = 'policyDefinition' 55 | $policySetDefinitionId = 'n/a' 56 | $policySetDefinitionDisplayName = 'n/a' 57 | $policySetDefinitionName = 'n/a' 58 | $policySetDefinitionType = 'n/a' 59 | } 60 | else { 61 | throw "unexpected .policyDefinitionId: $($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties)" 62 | } 63 | 64 | switch ($nonCompliant.assignmentId) { 65 | { $_ -like '/subscriptions/*' } { 66 | $policyAssignmentScopeType = 'Sub' 67 | } 68 | { $_ -like '/subscriptions/*/resourcegroups/*' } { 69 | $policyAssignmentScopeType = 'RG' 70 | } 71 | { $_ -like '/providers/Microsoft.Management/managementGroups/*' } { 72 | $policyAssignmentScopeType = 'MG' 73 | } 74 | default { 75 | $policyAssignmentScopeType = 'notDetected' 76 | } 77 | } 78 | 79 | $null = $script:arrayRemediatable.Add([PSCustomObject]@{ 80 | policyAssignmentScopeType = $policyAssignmentScopeType 81 | policyAssignmentScope = $nonCompliant.assignmentScope 82 | policyAssignmentId = $nonCompliant.assignmentId 83 | policyAssignmentName = $nonCompliant.assignmentName 84 | policyAssignmentDisplayName = $htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.displayName 85 | policyAssignmentPolicyOrPolicySet = $policyAssignmentPolicyOrPolicySet 86 | effect = $nonCompliant.effect 87 | policyDefinitionId = $nonCompliant.definitionId 88 | policyDefinitionName = $nonCompliant.definitionName 89 | policyDefinitionDisplayName = $htCacheDefinitionsPolicy.($nonCompliant.definitionId.toLower()).Json.properties.displayName 90 | policyDefinitionType = $htCacheDefinitionsPolicy.($nonCompliant.definitionId.toLower()).Type 91 | policySetPolicyDefinitionReferenceId = $nonCompliant.policyDefinitionReferenceId 92 | policySetDefinitionId = $policySetDefinitionId 93 | policySetDefinitionName = $policySetDefinitionName 94 | policySetDefinitionDisplayName = $policySetDefinitionDisplayName 95 | policySetDefinitionType = $policySetDefinitionType 96 | nonCompliantResourcesCount = $nonCompliant.count_ 97 | }) 98 | } 99 | else { 100 | Write-Host " skipping `$htCacheAssignmentsPolicy.($($nonCompliant.assignmentId)) potentially an assignment on an out-of-scope subscription" 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pwsh/dev/functions/getPrivateEndpointCapableResourceTypes.ps1: -------------------------------------------------------------------------------- 1 | function getPrivateEndpointCapableResourceTypes { 2 | $startGetAvailablePrivateEndpointTypes = Get-Date 3 | $privateEndpointAvailabilityCheckCompleted = $false 4 | $subsToProcessForGettingPrivateEndpointTypes = [System.Collections.ArrayList]@() 5 | $prioCounter = 0 6 | foreach ($subscription in $subsToProcessInCustomDataCollection) { 7 | $prioCounter++ 8 | if ($subscription.subscriptionId -eq $azAPICallConf['checkcontext'].Subscription.Id) { 9 | $null = $subsToProcessForGettingPrivateEndpointTypes.Add([PSCustomObject]@{ 10 | subscriptionInfo = $subscription 11 | prio = 0 12 | }) 13 | } 14 | else { 15 | $null = $subsToProcessForGettingPrivateEndpointTypes.Add([PSCustomObject]@{ 16 | subscriptionInfo = $subscription 17 | prio = $prioCounter 18 | }) 19 | } 20 | } 21 | 22 | foreach ($subscription in $subsToProcessForGettingPrivateEndpointTypes | Sort-Object -Property prio) { 23 | 24 | if ($privateEndpointAvailabilityCheckCompleted) { 25 | continue 26 | } 27 | 28 | $subscriptionId = $subscription.subscriptionInfo.subscriptionId 29 | $subscriptionName = $subscription.subscriptionInfo.subscriptionName 30 | 31 | $armLocationsFromAzAPICall = $azAPICallConf['htParameters'].ARMLocations 32 | 33 | Write-Host "Getting 'Available Private Endpoint Types' for Subscription '$($subscriptionName)' ($($subscriptionId)) for $($armLocationsFromAzAPICall.Count) physical locations" 34 | 35 | $batchSize = [math]::ceiling($armLocationsFromAzAPICall.Count / $ThrottleLimit) 36 | Write-Host "Optimal batch size: $($batchSize)" 37 | $counterBatch = [PSCustomObject] @{ Value = 0 } 38 | $locationsBatch = ($armLocationsFromAzAPICall) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } 39 | Write-Host "Processing data in $($locationsBatch.Count) batches" 40 | 41 | $locationsBatch | ForEach-Object -Parallel { 42 | 43 | $subscriptionId = $using:subscriptionId 44 | $azAPICallConf = $using:azAPICallConf 45 | $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes 46 | 47 | foreach ($location in $_.Group) { 48 | $currentTask = "Getting 'Available Private Endpoint Types' for location $($location)" 49 | #Write-Host $currentTask 50 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subscriptionId)/providers/Microsoft.Network/locations/$($location)/availablePrivateEndpointTypes?api-version=2022-07-01" 51 | $method = 'GET' 52 | $availablePrivateEndpointTypes = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -skipOnErrorCode 400, 409 53 | Write-Host " Returned $($availablePrivateEndpointTypes.Count) 'Available Private Endpoint Types' for location $($location)" 54 | foreach ($availablePrivateEndpointType in $availablePrivateEndpointTypes) { 55 | if (-not $htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower())) { 56 | $script:htAvailablePrivateEndpointTypes.(($availablePrivateEndpointType.resourceName).ToLower()) = @{} 57 | } 58 | } 59 | } 60 | } -ThrottleLimit $ThrottleLimit 61 | 62 | if ($htAvailablePrivateEndpointTypes.Keys.Count -gt 0) { 63 | #Write-Host " Created ht for $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types'" 64 | $privateEndpointAvailabilityCheckCompleted = $true 65 | } 66 | else { 67 | Write-Host " $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types' - likely the Resource Provider 'Microsoft.Network' is not registered - trying next available subscription" 68 | $privateEndpointAvailabilityCheckCompleted = $false 69 | } 70 | } 71 | 72 | if ($htAvailablePrivateEndpointTypes.Keys.Count -gt 0) { 73 | Write-Host " Created ht for $($htAvailablePrivateEndpointTypes.Keys.Count) 'Available Private Endpoint Types'" 74 | } 75 | else { 76 | $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 ''" 77 | Write-Host $throwmsg -ForegroundColor DarkRed 78 | Throw $throwmsg 79 | } 80 | 81 | $endGetAvailablePrivateEndpointTypes = Get-Date 82 | Write-Host "Getting 'Available Private Endpoint Types' duration: $((New-TimeSpan -Start $startGetAvailablePrivateEndpointTypes -End $endGetAvailablePrivateEndpointTypes).TotalMinutes) minutes ($((New-TimeSpan -Start $startGetAvailablePrivateEndpointTypes -End $endGetAvailablePrivateEndpointTypes).TotalSeconds) seconds)" 83 | #endregion Getting Available Private Endpoint Types 84 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getResourceDiagnosticsCapability.ps1: -------------------------------------------------------------------------------- 1 | function getResourceDiagnosticsCapability { 2 | Write-Host 'Checking Resource Types Diagnostics capability (1st party only)' 3 | $startResourceDiagnosticsCheck = Get-Date 4 | if (($resourcesAll).count -gt 0) { 5 | 6 | $startGroupResourceIdsByType = Get-Date 7 | $script:resourceTypesUnique = ($resourcesIdsAll | Group-Object -Property type) 8 | $endGroupResourceIdsByType = Get-Date 9 | Write-Host " GroupResourceIdsByType processing duration: $((New-TimeSpan -Start $startGroupResourceIdsByType -End $endGroupResourceIdsByType).TotalSeconds) seconds)" 10 | $resourceTypesUniqueCount = ($resourceTypesUnique | Measure-Object).count 11 | Write-Host " $($resourceTypesUniqueCount) unique Resource Types" 12 | $script:resourceTypesSummarizedArray = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) 13 | 14 | $script:resourceTypesDiagnosticsArray = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) 15 | $microsoftResourceTypes = $resourceTypesUnique.where({ $_.Name.StartsWith('microsoft') }) 16 | if ($microsoftResourceTypes.Count -gt 0) { 17 | $microsoftResourceTypes | ForEach-Object -Parallel { 18 | $resourceTypesUniqueGroup = $_ 19 | $resourcetype = $resourceTypesUniqueGroup.Name 20 | #region UsingVARs 21 | #fromOtherFunctions 22 | $azAPICallConf = $using:azAPICallConf 23 | $scriptPath = $using:ScriptPath 24 | #Array&HTs 25 | $ExcludedResourceTypesDiagnosticsCapable = $using:ExcludedResourceTypesDiagnosticsCapable 26 | $resourceTypesDiagnosticsArray = $using:resourceTypesDiagnosticsArray 27 | $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource 28 | $resourceTypesSummarizedArray = $using:resourceTypesSummarizedArray 29 | #endregion UsingVARs 30 | 31 | $skipThisResourceType = $false 32 | if (($ExcludedResourceTypesDiagnosticsCapable).Count -gt 0) { 33 | foreach ($excludedResourceType in $ExcludedResourceTypesDiagnosticsCapable) { 34 | if ($excludedResourceType -eq $resourcetype) { 35 | $skipThisResourceType = $true 36 | } 37 | } 38 | } 39 | 40 | if ($skipThisResourceType -eq $false) { 41 | $resourceCount = $resourceTypesUniqueGroup.Count 42 | 43 | #thx @Jim Britt (Microsoft) https://github.com/JimGBritt/AzurePolicy/tree/master/AzureMonitor/Scripts Create-AzDiagPolicy.ps1 44 | $responseJSON = '' 45 | $logCategories = [System.Collections.ArrayList]@() 46 | $metrics = $false 47 | $logs = $false 48 | 49 | $resourceAvailability = ($resourceCount - 1) 50 | $counterTryForResourceType = 0 51 | do { 52 | $counterTryForResourceType++ 53 | if ($resourceCount -gt 1) { 54 | $resourceId = $resourceTypesUniqueGroup.Group.Id[$resourceAvailability] 55 | } 56 | else { 57 | $resourceId = $resourceTypesUniqueGroup.Group.Id 58 | } 59 | 60 | $resourceAvailability = $resourceAvailability - 1 61 | if ($resourceId -like '*+*') { 62 | Write-Host "resourceId '$resourceId' contains bad character '+'; skipping resourceId" 63 | $responseJSON = 'skipResource' 64 | } 65 | else { 66 | $currentTask = "Checking if ResourceType '$resourceType' is capable for Resource Diagnostics using $counterTryForResourceType ResourceId: '$($resourceId)'" 67 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/$($resourceId)/providers/microsoft.insights/diagnosticSettingsCategories?api-version=2021-05-01-preview" 68 | $method = 'GET' 69 | $responseJSON = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri ([uri]::EscapeUriString($uri)) -method $method -currentTask $currentTask 70 | } 71 | 72 | if ($responseJSON -ne 'skipResource') { 73 | if ($responseJSON -eq 'ResourceTypeOrResourceProviderNotSupported') { 74 | Write-Host " ResourceTypeOrResourceProviderNotSupported | The resource type '$($resourcetype)' does not support diagnostic settings." 75 | 76 | } 77 | else { 78 | Write-Host " ResourceTypeSupported | The resource type '$($resourcetype)' supports diagnostic settings." 79 | } 80 | } 81 | else { 82 | Write-Host "resId '$resourceId' skipped" 83 | } 84 | } 85 | until ($resourceAvailability -lt 0 -or $responseJSON -ne 'skipResource') 86 | 87 | if ($resourceAvailability -lt 0 -and $responseJSON -eq 'skipResource') { 88 | Write-Host "tried for all available resourceIds ($($resourceCount)) for resourceType $resourceType, but seems all resourceIds needed to be skipped" 89 | $null = $script:resourceTypesDiagnosticsArray.Add([PSCustomObject]@{ 90 | ResourceType = $resourcetype 91 | Metrics = "n/a - $responseJSON" 92 | Logs = "n/a - $responseJSON" 93 | LogCategories = 'n/a' 94 | ResourceCount = $resourceCount 95 | }) 96 | } 97 | else { 98 | if ($responseJSON) { 99 | foreach ($response in $responseJSON) { 100 | if ($response.properties.categoryType -eq 'Metrics') { 101 | $metrics = $true 102 | } 103 | if ($response.properties.categoryType -eq 'Logs') { 104 | $logs = $true 105 | $null = $logCategories.Add($response.name) 106 | } 107 | } 108 | } 109 | 110 | $null = $script:resourceTypesDiagnosticsArray.Add([PSCustomObject]@{ 111 | ResourceType = $resourcetype 112 | Metrics = $metrics 113 | Logs = $logs 114 | LogCategories = $logCategories 115 | ResourceCount = $resourceCount 116 | }) 117 | } 118 | } 119 | else { 120 | Write-Host "Skipping ResourceType $($resourcetype) as per parameter '-ExcludedResourceTypesDiagnosticsCapable'" 121 | } 122 | } -ThrottleLimit $ThrottleLimit 123 | } 124 | else { 125 | Write-Host ' No 1st party Resource Types at all' 126 | } 127 | 128 | } 129 | else { 130 | Write-Host ' No Resources at all' 131 | } 132 | $endResourceDiagnosticsCheck = Get-Date 133 | Write-Host "Checking Resource Types Diagnostics capability duration: $((New-TimeSpan -Start $startResourceDiagnosticsCheck -End $endResourceDiagnosticsCheck).TotalMinutes) minutes ($((New-TimeSpan -Start $startResourceDiagnosticsCheck -End $endResourceDiagnosticsCheck).TotalSeconds) seconds)" 134 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getSubscriptions.ps1: -------------------------------------------------------------------------------- 1 | function getSubscriptions { 2 | $startGetSubscriptions = Get-Date 3 | $currentTask = 'Getting all Subscriptions' 4 | Write-Host "$currentTask" 5 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions?api-version=2020-01-01" 6 | $method = 'GET' 7 | $requestAllSubscriptionsAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask 8 | 9 | $script:htAllSubscriptionsFromAPI = @{} 10 | $script:htSubscriptionsFromOtherTenants = @{} 11 | 12 | Write-Host " $($requestAllSubscriptionsAPI.Count) Subscriptions returned" 13 | foreach ($subscription in $requestAllSubscriptionsAPI) { 14 | 15 | if ($subscription.tenantId -ne $azAPICallConf['checkcontext'].tenant.id) { 16 | Write-Host " Finding: $($subscription.displayName) ($($subscription.subscriptionId)) belongs to foreign tenant '$($subscription.tenantId)' - Azure Governance Visualizer: excluding this Subscripion" -ForegroundColor DarkRed 17 | $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId) = @{ 18 | subDetails = $subscription 19 | } 20 | } 21 | else { 22 | $script:htAllSubscriptionsFromAPI.($subscription.subscriptionId) = @{ 23 | subDetails = $subscription 24 | } 25 | } 26 | } 27 | Write-Host " $($htAllSubscriptionsFromAPI.Keys.Count) Subscriptions relevant" 28 | 29 | $endGetSubscriptions = Get-Date 30 | Write-Host "Getting all Subscriptions duration: $((New-TimeSpan -Start $startGetSubscriptions -End $endGetSubscriptions).TotalSeconds) seconds" 31 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/getTenantDetails.ps1: -------------------------------------------------------------------------------- 1 | function getTenantDetails { 2 | $currentTask = 'Get Tenant details' 3 | Write-Host $currentTask 4 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/tenants?api-version=2020-01-01" 5 | $method = 'GET' 6 | $tenantDetailsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask 7 | 8 | if (($tenantDetailsResult).count -gt 0) { 9 | $tenantDetails = $tenantDetailsResult.where({ $_.tenantId -eq ($azAPICallConf['checkContext']).Tenant.Id }) 10 | if ($tenantDetails.displayName) { 11 | $script:tenantDisplayName = $tenantDetails.displayName 12 | Write-Host " Tenant DisplayName: $tenantDisplayName" 13 | } 14 | else { 15 | Write-Host ' Tenant DisplayName: could not be retrieved' 16 | } 17 | 18 | if ($tenantDetails.defaultDomain) { 19 | $script:tenantDefaultDomain = $tenantDetails.defaultDomain 20 | } 21 | } 22 | else { 23 | Write-Host ' something unexpected' 24 | } 25 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/handleCloudEnvironment.ps1: -------------------------------------------------------------------------------- 1 | function handleCloudEnvironment { 2 | Write-Host "Environment: $($azAPICallConf['checkContext'].Environment.Name)" 3 | if ($DoAzureConsumption) { 4 | if ($azAPICallConf['checkContext'].Environment.Name -eq 'AzureChinaCloud') { 5 | Write-Host 'Azure Billing not supported in AzureChinaCloud, skipping Consumption..' 6 | $script:DoAzureConsumption = $false 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/namingValidation.ps1: -------------------------------------------------------------------------------- 1 | function NamingValidation($toCheck) { 2 | $checks = @(':', '/', '\', '<', '>', '|', '"') 3 | $array = [System.Collections.ArrayList]@() 4 | foreach ($check in $checks) { 5 | if ($toCheck -like "*$($check)*") { 6 | $null = $array.Add($check) 7 | } 8 | } 9 | if ($toCheck -match '\*') { 10 | $null = $array.Add('*') 11 | } 12 | if ($toCheck -match '\?') { 13 | $null = $array.Add('?') 14 | } 15 | return $array 16 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/prepareData.ps1: -------------------------------------------------------------------------------- 1 | function prepareData { 2 | Write-Host 'Preparing Data' 3 | $startPreparingArrays = Get-Date 4 | $script:optimizedTableForPathQuery = ($newTable | Select-Object -Property level, mg*, subscription*) | Sort-Object -Property level, mgid, subscriptionId -Unique 5 | $hlperOptimizedTableForPathQuery = $optimizedTableForPathQuery.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) } ) 6 | $script:optimizedTableForPathQueryMgAndSub = ($hlperOptimizedTableForPathQuery | Select-Object -Property level, mg*, subscription*) | Sort-Object -Property level, mgid, mgname, mgparentId, mgparentName, subscriptionId, subscription -Unique 7 | $script:optimizedTableForPathQueryMg = ($optimizedTableForPathQuery.where( { [String]::IsNullOrEmpty($_.SubscriptionId) } ) | Select-Object -Property level, mgid, mgName, mgparentid, mgparentName) | Sort-Object -Property level, mgid, mgname, mgparentId, mgparentName -Unique 8 | $script:optimizedTableForPathQuerySub = ($hlperOptimizedTableForPathQuery | Select-Object -Property subscription*) | Sort-Object -Property subscriptionId -Unique 9 | 10 | foreach ($entry in $optimizedTableForPathQuery) { 11 | $mgSubs = $optimizedTableForPathQueryMgAndSub.where( { $_.mgId -eq $entry.mgId } ) 12 | $mgChildren = ($optimizedTableForPathQueryMg.where( { $_.mgParentId -eq $entry.mgId } )).MgId 13 | $script:htMgDetails.($entry.mgId) = @{ 14 | subscriptionsCount = $mgSubs.Count 15 | subscriptions = $mgSubs 16 | details = $entry 17 | mgChildren = $mgChildren 18 | mgChildrenCount = $mgChildren.Count 19 | } 20 | 21 | 22 | } 23 | 24 | foreach ($entry in $optimizedTableForPathQueryMgAndSub) { 25 | $script:htSubDetails.($entry.SubscriptionId) = @{ 26 | details = $optimizedTableForPathQueryMgAndSub.where( { $_.SubscriptionId -eq $entry.SubscriptionId } ) 27 | } 28 | } 29 | 30 | $script:parentMgBaseQuery = ($optimizedTableForPathQueryMg.where( { $_.MgParentId -eq $getMgParentId } )) 31 | $script:parentMgNamex = $parentMgBaseQuery.mgParentName | Get-Unique 32 | $script:parentMgIdx = $parentMgBaseQuery.mgParentId | Get-Unique 33 | 34 | $endPreparingArrays = Get-Date 35 | Write-Host "Preparing Arrays duration: $((New-TimeSpan -Start $startPreparingArrays -End $endPreparingArrays).TotalMinutes) minutes ($((New-TimeSpan -Start $startPreparingArrays -End $endPreparingArrays).TotalSeconds) seconds)" 36 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/processAADGroups.ps1: -------------------------------------------------------------------------------- 1 | function processAADGroups { 2 | if ($NoPIMEligibility) { 3 | Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment exists)' 4 | } 5 | else { 6 | Write-Host 'Resolving Microsoft Entra groups (for which a RBAC role assignment or PIM eligibility exists)' 7 | } 8 | 9 | Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before resolving Microsoft Entra groups)" 10 | $startAADGroupsResolveMembers = Get-Date 11 | 12 | $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique 13 | $optimizedTableForAADGroupsQuery = [System.Collections.ArrayList]@() 14 | if ($roleAssignmentsforGroups.Count -gt 0) { 15 | foreach ($roleAssignmentforGroups in $roleAssignmentsforGroups) { 16 | $null = $optimizedTableForAADGroupsQuery.Add($roleAssignmentforGroups) 17 | } 18 | } 19 | 20 | $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count 21 | Write-Host " $aadGroupsCount Groups from RoleAssignments" 22 | 23 | if (-not $NoPIMEligibility) { 24 | $PIMEligibleGroups = $arrayPIMEligible.where({ $_.IdentityType -eq 'Group' }) | Select-Object IdentityObjectId, IdentityDisplayName | Sort-Object -Property IdentityObjectId -Unique 25 | $cntPIMEligibleGroupsTotal = 0 26 | $cntPIMEligibleGroupsNotCoveredFromRoleAssignments = 0 27 | foreach ($PIMEligibleGroup in $PIMEligibleGroups) { 28 | $cntPIMEligibleGroupsTotal++ 29 | if ($optimizedTableForAADGroupsQuery.RoleAssignmentIdentityObjectId -notcontains $PIMEligibleGroup.IdentityObjectId) { 30 | $cntPIMEligibleGroupsNotCoveredFromRoleAssignments++ 31 | $null = $optimizedTableForAADGroupsQuery.Add([PSCustomObject]@{ 32 | RoleAssignmentIdentityObjectId = $PIMEligibleGroup.IdentityObjectId 33 | RoleAssignmentIdentityDisplayname = $PIMEligibleGroup.IdentityDisplayName 34 | }) 35 | } 36 | } 37 | Write-Host " $cntPIMEligibleGroupsTotal groups from PIM eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in role assignments)" 38 | $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count 39 | Write-Host " $aadGroupsCount groups from role assignments and PIM eligibility" 40 | } 41 | 42 | if ($aadGroupsCount -gt 0) { 43 | 44 | switch ($aadGroupsCount) { 45 | { $_ -gt 0 } { $indicator = 1 } 46 | { $_ -gt 10 } { $indicator = 5 } 47 | { $_ -gt 50 } { $indicator = 10 } 48 | { $_ -gt 100 } { $indicator = 20 } 49 | { $_ -gt 250 } { $indicator = 25 } 50 | { $_ -gt 500 } { $indicator = 50 } 51 | { $_ -gt 1000 } { $indicator = 100 } 52 | { $_ -gt 10000 } { $indicator = 250 } 53 | } 54 | 55 | Write-Host " processing $($aadGroupsCount) Microsoft Entra groups (indicating progress in steps of $indicator)" 56 | 57 | $ThrottleLimitThis = $ThrottleLimit * 2 58 | $batchSize = [math]::ceiling($optimizedTableForAADGroupsQuery.Count / $ThrottleLimitThis) 59 | Write-Host "Optimal batch size: $($batchSize)" 60 | $counterBatch = [PSCustomObject] @{ Value = 0 } 61 | $optimizedTableForAADGroupsQueryBatch = ($optimizedTableForAADGroupsQuery) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } 62 | Write-Host "Processing data in $($optimizedTableForAADGroupsQueryBatch.Count) batches" 63 | 64 | $optimizedTableForAADGroupsQueryBatch | ForEach-Object -Parallel { 65 | #$aadGroupIdWithRoleAssignment = $_ 66 | #region UsingVARs 67 | #fromOtherFunctions 68 | $AADGroupMembersLimit = $using:AADGroupMembersLimit 69 | $azAPICallConf = $using:azAPICallConf 70 | $scriptPath = $using:ScriptPath 71 | #Array&HTs 72 | $htAADGroupsDetails = $using:htAADGroupsDetails 73 | $arrayGroupRoleAssignmentsOnServicePrincipals = $using:arrayGroupRoleAssignmentsOnServicePrincipals 74 | $arrayGroupRequestResourceNotFound = $using:arrayGroupRequestResourceNotFound 75 | $arrayProgressedAADGroups = $using:arrayProgressedAADGroups 76 | $htAADGroupsExeedingMemberLimit = $using:htAADGroupsExeedingMemberLimit 77 | $indicator = $using:indicator 78 | $htUserTypesGuest = $using:htUserTypesGuest 79 | $htServicePrincipals = $using:htServicePrincipals 80 | #other 81 | $function:getGroupmembers = $using:funcGetGroupmembers 82 | #endregion UsingVARs 83 | 84 | foreach ($aadGroupIdWithRoleAssignment in $_.Group) { 85 | 86 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)/transitiveMembers/`$count" 87 | $method = 'GET' 88 | $aadGroupMembersCount = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupMembersCountTransitive $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)" -listenOn 'Content' -consistencyLevel 'eventual' 89 | 90 | if ($aadGroupMembersCount -eq 'Request_ResourceNotFound') { 91 | $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ 92 | groupId = $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId 93 | }) 94 | } 95 | else { 96 | if ($aadGroupMembersCount -gt $AADGroupMembersLimit) { 97 | Write-Host " Group exceeding limit ($($AADGroupMembersLimit)); memberCount: $aadGroupMembersCount; Group: $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname) ($($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)); Members will not be resolved adjust the limit using parameter -AADGroupMembersLimit" 98 | $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) = @{ 99 | MembersAllCount = $aadGroupMembersCount 100 | MembersUsersCount = 'n/a' 101 | MembersGroupsCount = 'n/a' 102 | MembersServicePrincipalsCount = 'n/a' 103 | } 104 | 105 | } 106 | else { 107 | getGroupmembers -aadGroupId $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId -aadGroupDisplayName $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname 108 | } 109 | } 110 | 111 | $null = $script:arrayProgressedAADGroups.Add($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) 112 | $processedAADGroupsCount = $null 113 | $processedAADGroupsCount = ($arrayProgressedAADGroups).Count 114 | if ($processedAADGroupsCount) { 115 | if ($processedAADGroupsCount % $indicator -eq 0) { 116 | Write-Host " $processedAADGroupsCount Microsoft Entra groups processed" 117 | } 118 | } 119 | } 120 | } -ThrottleLimit ($ThrottleLimitThis) 121 | } 122 | else { 123 | Write-Host " processing $($aadGroupsCount) Microsoft Entra groups" 124 | } 125 | 126 | $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count 127 | if ($arrayGroupRequestResourceNotFoundCount -gt 0) { 128 | Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" 129 | } 130 | 131 | Write-Host " processed $($arrayProgressedAADGroups.Count) Microsoft Entra groups" 132 | $endAADGroupsResolveMembers = Get-Date 133 | Write-Host "Resolving Microsoft Entra groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" 134 | Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after resolving Microsoft Entra groups)" 135 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/processApplications.ps1: -------------------------------------------------------------------------------- 1 | function processApplications { 2 | Write-Host 'Processing Service Principals - Applications' 3 | $script:servicePrincipalsOfTypeApplication = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq 'Application' -and $htServicePrincipals.($_).appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Subscription.TenantId } ) 4 | if ($azAPICallConf['htParameters'].userType -eq 'Guest') { 5 | #checking if Guest has enough permissions 6 | $app4Test = $htServicePrincipals.($servicePrincipalsOfTypeApplication[0]) 7 | $currentTask = "getApp Test $($app4Test.appId)" 8 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($app4Test.appId)'" 9 | $method = 'GET' 10 | $testGetApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask 11 | if ($testGetApplication -eq 'skipApplications') { 12 | $skipApplications = $true 13 | Write-Host ' Guest account does not have enough permissions, skipping Applications (Secrets & Certificates)' 14 | } 15 | } 16 | if (-not $skipApplications) { 17 | $startSPApp = Get-Date 18 | $currentDateUTC = (Get-Date).ToUniversalTime() 19 | $script:arrayApplicationRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) 20 | 21 | $ThrottleLimitThis = $ThrottleLimit * 2 22 | $batchSize = [math]::ceiling($servicePrincipalsOfTypeApplication.Count / $ThrottleLimitThis) 23 | Write-Host "Optimal batch size: $($batchSize)" 24 | $counterBatch = [PSCustomObject] @{ Value = 0 } 25 | $servicePrincipalsOfTypeApplicationBatch = ($servicePrincipalsOfTypeApplication) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } 26 | Write-Host "Processing data in $($servicePrincipalsOfTypeApplicationBatch.Count) batches" 27 | 28 | $servicePrincipalsOfTypeApplicationBatch | ForEach-Object -Parallel { 29 | 30 | #region UsingVARs 31 | $currentDateUTC = $using:currentDateUTC 32 | #fromOtherFunctions 33 | $azAPICallConf = $using:azAPICallConf 34 | $scriptPath = $using:ScriptPath 35 | #Array&HTs 36 | $arrayApplicationRequestResourceNotFound = $using:arrayApplicationRequestResourceNotFound 37 | $htAppDetails = $using:htAppDetails 38 | $htServicePrincipals = $using:htServicePrincipals 39 | #endregion UsingVARs 40 | 41 | foreach ($entry in $_.Group) { 42 | $sp = $htServicePrincipals.($entry) 43 | 44 | $currentTask = "getApp $($sp.appId)" 45 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" 46 | $method = 'GET' 47 | $getApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask 48 | 49 | if ($getApplication -eq 'Request_ResourceNotFound') { 50 | $null = $script:arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ 51 | appId = $sp.appId 52 | }) 53 | } 54 | else { 55 | if (($getApplication).Count -eq 0) { 56 | Write-Host "$($sp.appId) no data returned / seems non existent?" 57 | } 58 | else { 59 | $script:htAppDetails.($sp.id) = @{ 60 | servicePrincipalType = $sp.servicePrincipalType 61 | spGraphDetails = $sp 62 | appGraphDetails = $getApplication 63 | } 64 | 65 | $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count 66 | if ($appPasswordCredentialsCount -gt 0) { 67 | $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount 68 | $appPasswordCredentialsExpiredCount = 0 69 | $appPasswordCredentialsGracePeriodExpiryCount = 0 70 | $appPasswordCredentialsExpiryOKCount = 0 71 | $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 72 | foreach ($appPasswordCredential in $getApplication.passwordCredentials) { 73 | $passwordExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays 74 | if ($passwordExpiryTotalDays -lt 0) { 75 | $appPasswordCredentialsExpiredCount++ 76 | } 77 | elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { 78 | $appPasswordCredentialsGracePeriodExpiryCount++ 79 | } 80 | else { 81 | if ($passwordExpiryTotalDays -gt 730) { 82 | $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ 83 | } 84 | else { 85 | $appPasswordCredentialsExpiryOKCount++ 86 | } 87 | } 88 | } 89 | $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount 90 | $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount 91 | $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount 92 | $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount 93 | } 94 | 95 | $appKeyCredentialsCount = ($getApplication.keyCredentials).count 96 | if ($appKeyCredentialsCount -gt 0) { 97 | $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount 98 | $appKeyCredentialsExpiredCount = 0 99 | $appKeyCredentialsGracePeriodExpiryCount = 0 100 | $appKeyCredentialsExpiryOKCount = 0 101 | $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 102 | foreach ($appKeyCredential in $getApplication.keyCredentials) { 103 | $keyCredentialExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays 104 | if ($keyCredentialExpiryTotalDays -lt 0) { 105 | $appKeyCredentialsExpiredCount++ 106 | } 107 | elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { 108 | $appKeyCredentialsGracePeriodExpiryCount++ 109 | } 110 | else { 111 | if ($keyCredentialExpiryTotalDays -gt 730) { 112 | $appKeyCredentialsExpiryOKMoreThan2YearsCount++ 113 | } 114 | else { 115 | $appKeyCredentialsExpiryOKCount++ 116 | } 117 | } 118 | } 119 | $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount 120 | $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount 121 | $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount 122 | $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount 123 | } 124 | } 125 | } 126 | } 127 | } -ThrottleLimit ($ThrottleLimitThis) 128 | 129 | $endSPApp = Get-Date 130 | Write-Host "Processing Service Principals - Applications duration: $((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalSeconds) seconds)" 131 | } 132 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/processDiagramMermaid.ps1: -------------------------------------------------------------------------------- 1 | function processDiagramMermaid() { 2 | if ($ManagementGroupId -ne $azAPICallConf['checkContext'].Tenant.Id) { 3 | $optimizedTableForPathQueryMg = $optimizedTableForPathQueryMg.where({ $_.mgParentId -ne "'upperScopes'" }) 4 | } 5 | $mgLevels = ($optimizedTableForPathQueryMg | Sort-Object -Property Level -Unique).Level 6 | 7 | foreach ($mgLevel in $mgLevels) { 8 | $mgsInLevel = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel } )).MgId | Get-Unique 9 | foreach ($mgInLevel in $mgsInLevel) { 10 | $mgDetails = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) 11 | $mgName = $mgDetails.MgName | Get-Unique 12 | $mgParentId = $mgDetails.mgParentId | Get-Unique 13 | $mgParentName = $mgDetails.mgParentName | Get-Unique 14 | if ($mgInLevel -ne $getMgParentId) { 15 | $null = $script:arrayMgs.Add($mgInLevel) 16 | } 17 | 18 | if ($mgParentName -eq $mgParentId) { 19 | $mgParentNameId = $mgParentName 20 | } 21 | else { 22 | $mgParentNameId = "$mgParentName
$mgParentId" 23 | } 24 | 25 | if ($mgName -eq $mgInLevel) { 26 | $mgNameId = $mgName 27 | } 28 | else { 29 | $mgNameId = "$mgName
$mgInLevel" 30 | } 31 | $script:markdownhierarchyMgs += @" 32 | $mgParentId(`"$mgParentNameId`") --> $mgInLevel(`"$mgNameId`")`n 33 | "@ 34 | $subsUnderMg = ($optimizedTableForPathQueryMgAndSub.where( { -not [string]::IsNullOrEmpty($_.SubscriptionId) -and $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )).SubscriptionId 35 | if (($subsUnderMg | Measure-Object).count -gt 0) { 36 | foreach ($subUnderMg in $subsUnderMg) { 37 | $null = $script:arraySubs.Add("SubsOf$mgInLevel") 38 | $mgDetalsN = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) 39 | $mgName = $mgDetalsN.MgName | Get-Unique 40 | $mgParentId = $mgDetalsN.MgParentId | Get-Unique 41 | $mgParentName = $mgDetalsN.MgParentName | Get-Unique 42 | $subName = ($optimizedTableForPathQuery.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel -and $_.SubscriptionId -eq $subUnderMg } )).Subscription | Get-Unique 43 | $script:markdownTable += @" 44 | | $mgLevel | $mgName | $mgInLevel | $mgParentName | $mgParentId | $subName | $($subUnderMg -replace '.*/') |`n 45 | "@ 46 | } 47 | $mgName = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )).MgName | Get-Unique 48 | if ($mgName -eq $mgInLevel) { 49 | $mgNameId = $mgName 50 | } 51 | else { 52 | $mgNameId = "$mgName
$mgInLevel" 53 | } 54 | $script:markdownhierarchySubs += @" 55 | $mgInLevel(`"$mgNameId`") --> SubsOf$mgInLevel(`"$(($subsUnderMg | Measure-Object).count)`")`n 56 | "@ 57 | } 58 | else { 59 | $mgDetailsM = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) 60 | $mgName = $mgDetailsM.MgName | Get-Unique 61 | $mgParentId = $mgDetailsM.MgParentId | Get-Unique 62 | $mgParentName = $mgDetailsM.MgParentName | Get-Unique 63 | $script:markdownTable += @" 64 | | $mgLevel | $mgName | $mgInLevel | $mgParentName | $mgParentId | none | none |`n 65 | "@ 66 | } 67 | 68 | if (($script:outOfScopeSubscriptions | Measure-Object).count -gt 0) { 69 | $subsoosUnderMg = ($outOfScopeSubscriptions.where({ $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel })).SubscriptionId | Get-Unique 70 | if (($subsoosUnderMg | Measure-Object).count -gt 0) { 71 | foreach ($subUnderMg in $subsoosUnderMg) { 72 | $null = $script:arraySubsOos.Add("SubsoosOf$mgInLevel") 73 | $mgDetalsN = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel } )) 74 | $mgName = $mgDetalsN.MgName | Get-Unique 75 | } 76 | $mgName = ($outOfScopeSubscriptions.where({ $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel })).ManagementGroupName | Get-Unique 77 | if ($mgName -eq $mgInLevel) { 78 | $mgNameId = $mgName 79 | } 80 | else { 81 | $mgNameId = "$mgName
$mgInLevel" 82 | } 83 | $script:markdownhierarchySubs += @" 84 | $mgInLevel(`"$mgNameId`") --> SubsoosOf$mgInLevel(`"$(($subsoosUnderMg | Measure-Object).count)`")`n 85 | "@ 86 | } 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/processHierarchyMapOnly.ps1: -------------------------------------------------------------------------------- 1 | function processHierarchyMapOnly { 2 | foreach ($entity in $htEntities.values) { 3 | if ($entity.parentNameChain -contains $ManagementGroupID -or $entity.Id -eq $ManagementGroupId) { 4 | 5 | if ($entity.type -eq '/subscriptions') { 6 | $hlpEntityParent = $htEntities.(($entity.parent)) 7 | addRowToTable ` 8 | -level (($entity.ParentNameChain).Count - 1) ` 9 | -mgName $hlpEntityParent.displayName ` 10 | -mgId ($entity.parent) ` 11 | -mgParentId $hlpEntityParent.Parent ` 12 | -mgParentName $hlpEntityParent.ParentDisplayName ` 13 | -Subscription $entity.DisplayName ` 14 | -SubscriptionId $entity.Id 15 | } 16 | if ($entity.type -eq 'Microsoft.Management/managementGroups') { 17 | addRowToTable ` 18 | -level ($entity.ParentNameChain).Count ` 19 | -mgName $entity.displayname ` 20 | -mgId $entity.id ` 21 | -mgParentId $entity.Parent ` 22 | -mgParentName $entity.ParentDisplayName 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/processHierarchyMapOnlyCustomData.ps1: -------------------------------------------------------------------------------- 1 | function processHierarchyMapOnlyCustomData { 2 | Write-Host 'HierarchyMapOnly with custom data' -ForegroundColor Yellow 3 | Write-Host ' Parameter HierarchyMapOnly:' $HierarchyMapOnly 4 | Write-Host ' Check if HierarchyMapOnlyCustomDataJSON is valid JSON' 5 | try { 6 | $HierarchyMapOnlyCustomDataConvertedAsHashTable = $HierarchyMapOnlyCustomDataJSON | ConvertFrom-Json -AsHashtable 7 | $hierarchyMapOnlyCustomData = @{} 8 | foreach ($key in $HierarchyMapOnlyCustomDataConvertedAsHashTable.Keys) { 9 | $hierarchyMapOnlyCustomData.$key = $HierarchyMapOnlyCustomDataConvertedAsHashTable.$key | ConvertTo-Json | ConvertFrom-Json 10 | } 11 | Write-Host ' HierarchyMapOnlyCustomDataJSON is valid JSON' -ForegroundColor Green 12 | } 13 | catch { 14 | throw 'HierarchyMapOnlyCustomDataJSON is not valid JSON' 15 | } 16 | 17 | Write-Host ' Parameter hierarchyMapOnlyCustomData count:' $hierarchyMapOnlyCustomData.Keys.Count 18 | 19 | #validate 20 | Write-Host ' ManagementGroupId validation' 21 | if (-not $ManagementGroupId) { 22 | throw 'ManagementGroupId validation failed - please provide ManagementGroupId (parameter -ManagementGroupId)' 23 | } 24 | else { 25 | if ($hierarchyMapOnlyCustomData.$ManagementGroupId) { 26 | Write-Host " ManagementGroupId '$ManagementGroupId' is available in 'hierarchyMapOnlyCustomData'" 27 | } 28 | else { 29 | throw "ManagementGroupId validation failed - Given ManagementGroupId '$ManagementGroupId' is NOT available in 'hierarchyMapOnlyCustomData'" 30 | } 31 | Write-Host " ManagementGroupId validation passed '$ManagementGroupId'" -ForegroundColor Green 32 | } 33 | 34 | Write-Host ' CustomData validation' 35 | if ($hierarchyMapOnlyCustomData.Keys.Count -gt 0) { 36 | Write-Host ' Checking Keys (sanity check on first item)' 37 | $requiredKeys = @('Id', 'ParentId', 'ParentNameChain', 'ParentDisplayName', 'DisplayName', 'type') 38 | $firstItem = $hierarchyMapOnlyCustomData.($($hierarchyMapOnlyCustomData.Keys)[0]) 39 | foreach ($requiredKey in $requiredKeys) { 40 | if (($firstitem | Get-Member -Name $requiredKey)) { 41 | Write-Host " Key:$($requiredKey) exists" -ForegroundColor Green 42 | } 43 | else { 44 | Write-Host " CustomData validation failed - required key:$($requiredKey) missing" -ForegroundColor DarkRed 45 | Write-Host " The following keys are expected: $($requiredKeys -join ', ')" 46 | throw "CustomData validation failed - required key:$($requiredKey) missing" 47 | } 48 | } 49 | 50 | Write-Host ' Checking for existence of Management Groups' 51 | $HierarchyMapOnlyCustomDataHroupedByType = $hierarchyMapOnlyCustomData.values | Group-Object -Property type 52 | if ($HierarchyMapOnlyCustomDataHroupedByType.Name -notcontains 'Microsoft.Management/managementGroups') { 53 | Write-Host ' CustomData validation failed - Custom data does not contain Manangement Groups' 54 | throw 'CustomData validation failed - Custom data does not contain Manangement Groups' 55 | } 56 | else { 57 | Write-Host ' Checking for existence of Management Groups passed' -ForegroundColor Green 58 | } 59 | foreach ($type in $HierarchyMapOnlyCustomDataHroupedByType) { 60 | Write-Host " Custom Data contains $($type.Count) x type: '$($type.name)'" 61 | } 62 | 63 | Write-Host ' CustomData validation passed' -ForegroundColor Green 64 | } 65 | else { 66 | Write-Host " CustomData validation failed - no data (`$hierarchyMapOnlyCustomData.Keys.Count: $($hierarchyMapOnlyCustomData.Keys.Count))" 67 | throw "CustomData validation failed - no data (`$hierarchyMapOnlyCustomData.Keys.Count: $($hierarchyMapOnlyCustomData.Keys.Count))" 68 | } 69 | $script:htEntities = $hierarchyMapOnlyCustomData 70 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/processMDfCCoverage.ps1: -------------------------------------------------------------------------------- 1 | function processMDfCCoverage { 2 | Write-Host ' Processing Defender Coverage' 3 | $start = Get-Date 4 | 5 | $htDefenderProps = @{} 6 | $htDefenderExtensions = @{} 7 | foreach ($x in $arrayDefenderPlans) { 8 | if (-not $htDefenderProps.($x.defenderPlan)) { 9 | $htDefenderProps.($x.defenderPlan) = [System.Collections.ArrayList]@() 10 | } 11 | if (-not $htDefenderExtensions.($x.defenderPlan)) { 12 | $htDefenderExtensions.($x.defenderPlan) = [System.Collections.ArrayList]@() 13 | } 14 | foreach ($noteprop in ($x.defenderPlanFull.properties | Get-Member).where({ $_.MemberType -eq 'NoteProperty' })) { 15 | if ($htDefenderProps.($x.defenderPlan) -notcontains $noteprop.Name) { 16 | $null = $htDefenderProps.($x.defenderPlan).Add($noteprop.Name) 17 | } 18 | if ($noteprop.Name -eq 'extensions') { 19 | foreach ($extension in $x.defenderPlanFull.properties.($noteprop.Name)) { 20 | if ($htDefenderExtensions.($x.defenderPlan) -notcontains $extension.name) { 21 | $null = $htDefenderExtensions.($x.defenderPlan).Add($extension.name) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | $arrayDefenderPlansNamesUnique = $arrayDefenderPlans.defenderPlan | Sort-Object -Unique 29 | $script:arrayDefenderPlansCoverage = [System.Collections.ArrayList]@() 30 | foreach ($defenderPlanName in $arrayDefenderPlansNamesUnique) { 31 | foreach ($defenderPlanEntry in $arrayDefenderPlans.where({ $_.defenderPlan -eq $defenderPlanName })) { 32 | $objDefenderPlan = [ordered]@{ 33 | plan = $defenderPlanEntry.defenderPlan 34 | subscriptionId = $defenderPlanEntry.subscriptionId 35 | subscriptionName = $defenderPlanEntry.subscriptionName 36 | subscriptionMgPath = $defenderPlanEntry.subscriptionMgPath 37 | } 38 | foreach ($prop in $htDefenderProps.($defenderPlanName)) { 39 | if ($prop -eq 'extensions') { 40 | foreach ($extension in $htDefenderExtensions.($defenderPlanName)) { 41 | $extensionObject = $defenderPlanEntry.defenderPlanFull.properties.extensions.where({ $_.name -eq $extension }) 42 | if ($extensionObject.count -gt 0) { 43 | $objDefenderPlan.("ext_$($extension)") = $extensionObject.isEnabled 44 | if ($defenderPlanName -eq 'StorageAccounts' -and $extension -eq 'OnUploadMalwareScanning') { 45 | if ($extensionObject.additionalExtensionProperties.CapGBPerMonthPerStorageAccount) { 46 | $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $extensionObject.additionalExtensionProperties.CapGBPerMonthPerStorageAccount 47 | } 48 | else { 49 | $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $null 50 | } 51 | } 52 | } 53 | else { 54 | $objDefenderPlan.("ext_$($extension)") = $null 55 | if ($defenderPlanName -eq 'StorageAccounts' -and $extension -eq 'OnUploadMalwareScanning') { 56 | $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $null 57 | } 58 | } 59 | } 60 | } 61 | elseif ($prop -eq 'replacedBy') { 62 | $objDefenderPlan.($prop) = $defenderPlanEntry.defenderPlanFull.properties.($prop) -join ';' 63 | } 64 | else { 65 | $objDefenderPlan.($prop) = $defenderPlanEntry.defenderPlanFull.properties.($prop) 66 | } 67 | 68 | if ($defenderPlanName -eq 'VirtualMachines' -and $prop -eq 'subPlan') { 69 | if ($defenderPlanEntry.defenderPlanFull.properties.($prop)) { 70 | if ($htSecuritySettings.($defenderPlanEntry.subscriptionId).WDATP) { 71 | $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = ($htSecuritySettings.($defenderPlanEntry.subscriptionId).WDATP.properties.enabled).ToString() 72 | } 73 | else { 74 | $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = 'unknown' 75 | } 76 | } 77 | else { 78 | $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = 'n/a' 79 | } 80 | 81 | } 82 | } 83 | $null = $script:arrayDefenderPlansCoverage.Add($objDefenderPlan) 84 | } 85 | } 86 | 87 | # $tstsmp = Get-Date -Format 'yyyyMMdd_HHmmss' 88 | # $arrayDefenderPlansCoverage | ConvertTo-Json -Depth 99 > "c:\temp\defenderCoverage_Final_$($tstsmp).json" 89 | 90 | $arrayDefenderPlanSpecificProperties = [System.Collections.ArrayList]@() 91 | $arrayDefenderPlanCommonProperties = @('plan', 'subscriptionId', 'subscriptionName', 'subscriptionMgPath', 'pricingTier', 'freeTrialRemainingTime') 92 | foreach ($plan in $arrayDefenderPlansCoverage) { 93 | $plan.Keys | ForEach-Object { 94 | if ($_ -notin $arrayDefenderPlanCommonProperties) { 95 | $null = $arrayDefenderPlanSpecificProperties.Add("$($plan.plan)_$($_)") 96 | } 97 | } 98 | } 99 | $arrayDefenderPlanSpecificPropertiesUnique = $arrayDefenderPlanSpecificProperties | Sort-Object -Unique 100 | 101 | $arrayDefenderPlansCoverageAll = [System.Collections.ArrayList]@() 102 | foreach ($entry in $arrayDefenderPlansCoverage) { 103 | $obj = [PSCustomObject]@{} 104 | foreach ($cprop in $arrayDefenderPlanCommonProperties) { 105 | $obj | Add-Member -MemberType NoteProperty -Name $cprop -Value $entry.($cprop) 106 | } 107 | foreach ($sprop in $arrayDefenderPlanSpecificPropertiesUnique) { 108 | if ($sprop -like "$($entry.plan)_*") { 109 | $obj | Add-Member -MemberType NoteProperty -Name $sprop -Value $entry.($sprop -replace "$($entry.plan)_", '' ) 110 | } 111 | else { 112 | $obj | Add-Member -MemberType NoteProperty -Name $sprop -Value $null 113 | } 114 | } 115 | $null = $arrayDefenderPlansCoverageAll.Add($obj) 116 | } 117 | 118 | if (-not $NoCsvExport) { 119 | Write-Host " Exporting MDfCCoverage CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCCoverage.csv'" 120 | $arrayDefenderPlansCoverageAll | Sort-Object -Property plan, subscriptionName | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCCoverage.csv" -Delimiter "$csvDelimiter" -NoTypeInformation 121 | } 122 | 123 | $end = Get-Date 124 | Write-Host " Defender Coverage processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" 125 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/processManagedIdentities.ps1: -------------------------------------------------------------------------------- 1 | function processManagedIdentities { 2 | Write-Host 'Processing Service Principals - Managed Identities' 3 | $startSPMI = Get-Date 4 | $script:servicePrincipalsOfTypeManagedIdentity = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq 'ManagedIdentity' } ) 5 | $script:servicePrincipalsOfTypeManagedIdentityCount = $servicePrincipalsOfTypeManagedIdentity.Count 6 | if ($servicePrincipalsOfTypeManagedIdentityCount -gt 0) { 7 | foreach ($sp in $servicePrincipalsOfTypeManagedIdentity) { 8 | $hlpSp = $htServicePrincipals.($sp) 9 | if ($hlpSp.alternativeNames -gt 0) { 10 | foreach ($usageentry in $hlpSp.alternativeNames) { 11 | if ($usageentry -like '*/providers/Microsoft.Authorization/policyAssignments/*') { 12 | $script:htManagedIdentityForPolicyAssignment.($hlpSp.Id) = @{ 13 | policyAssignmentId = $usageentry.ToLower() 14 | } 15 | $script:htPolicyAssignmentManagedIdentity.($usageentry.ToLower()) = @{ 16 | miObjectId = $hlpSp.id 17 | } 18 | if (-not $htManagedIdentityDisplayName.($hlpSp.displayName)) { 19 | $script:htManagedIdentityDisplayName.("$($hlpSp.displayName)_$($usageentry.ToLower())") = $hlpSp 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | $endSPMI = Get-Date 27 | Write-Host "Processing Service Principals - Managed Identities duration: $((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalSeconds) seconds)" 28 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/removeInvalidFileNameChars.ps1: -------------------------------------------------------------------------------- 1 | function removeInvalidFileNameChars { 2 | param( 3 | [Parameter(Mandatory = $true)] 4 | [string] 5 | $Name 6 | ) 7 | 8 | if ($Name -like '`[Deprecated`]:*') { 9 | $Name = $Name -replace '\[Deprecated\]\:', '[Deprecated]' 10 | } 11 | if ($Name -like '`[Preview`]:*') { 12 | $Name = $Name -replace '\[Preview\]\:', '[Preview]' 13 | } 14 | if ($Name -like '`[ASC Private Preview`]:*') { 15 | $Name = $Name -replace '\[ASC Private Preview\]\:', '[ASC Private Preview]' 16 | } 17 | 18 | return ($Name -replace ':', '_' -replace '/', '_' -replace '\\', '_' -replace '<', '_' -replace '>', '_' -replace '\*', '_' -replace '\?', '_' -replace '\|', '_' -replace '"', '_') 19 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/selectMg.ps1: -------------------------------------------------------------------------------- 1 | function selectMg() { 2 | Write-Host 'Please select a Management Group from the list below:' 3 | $MgtGroupArray | Select-Object '#', Name, @{Name = 'displayName'; Expression = { $_.properties.displayName } }, Id | Format-Table 4 | Write-Host "If you don't see your ManagementGroupID try using the parameter -ManagementGroupID" -ForegroundColor Yellow 5 | if ($msg) { 6 | Write-Host $msg -ForegroundColor Red 7 | } 8 | 9 | $script:SelectedMG = Read-Host "Please enter a selection from 1 to $(($MgtGroupArray).count)" 10 | 11 | if ($SelectedMG -match '^[\d\.]+$') { 12 | if ([int]$SelectedMG -lt 1 -or [int]$SelectedMG -gt ($MgtGroupArray).count) { 13 | $msg = "last input '$SelectedMG' is out of range, enter a number from the selection!" 14 | selectMg 15 | } 16 | } 17 | else { 18 | $msg = "last input '$SelectedMG' is not numeric, enter a number from the selection!" 19 | selectMg 20 | } 21 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/setBaseVariablesMG.ps1: -------------------------------------------------------------------------------- 1 | function setBaseVariablesMG { 2 | if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { 3 | $script:mgSubPathTopMg = $selectedManagementGroupId.ParentName 4 | $script:getMgParentId = $selectedManagementGroupId.ParentName 5 | $script:getMgParentName = $selectedManagementGroupId.ParentDisplayName 6 | $script:mermaidprnts = "'$(($azAPICallConf['checkContext']).Tenant.Id)',$getMgParentId" 7 | } 8 | else { 9 | $script:hierarchyLevel = -1 10 | $script:mgSubPathTopMg = "$ManagementGroupId" 11 | $script:getMgParentId = "'$ManagementGroupId'" 12 | $script:getMgParentName = 'Tenant Root' 13 | $script:mermaidprnts = "'$getMgParentId',$getMgParentId" 14 | } 15 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/setOutput.ps1: -------------------------------------------------------------------------------- 1 | function setOutput { 2 | if (-not [IO.Path]::IsPathRooted($outputPath)) { 3 | $outputPath = Join-Path -Path (Get-Location).Path -ChildPath $outputPath 4 | } 5 | $outputPath = Join-Path -Path $outputPath -ChildPath '.' 6 | $script:outputPath = [IO.Path]::GetFullPath($outputPath) 7 | if (-not (Test-Path $outputPath)) { 8 | Write-Host "path $outputPath does not exist - please create it!" -ForegroundColor Red 9 | Throw 'Error - check the last console output for details' 10 | } 11 | else { 12 | Write-Host "Output/Files will be created in path '$outputPath'" 13 | } 14 | 15 | #fileTimestamp 16 | try { 17 | $script:fileTimestamp = (Get-Date -Format $FileTimeStampFormat) 18 | } 19 | catch { 20 | Write-Host "fileTimestamp format: '$($FileTimeStampFormat)' invalid; continue with default format: 'yyyyMMdd_HHmmss'" -ForegroundColor Red 21 | $FileTimeStampFormat = 'yyyyMMdd_HHmmss' 22 | $script:fileTimestamp = (Get-Date -Format $FileTimeStampFormat) 23 | } 24 | 25 | $script:executionDateTimeInternationalReadable = Get-Date -Format 'dd-MMM-yyyy HH:mm:ss' 26 | $script:currentTimeZone = (Get-TimeZone).Id 27 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/setTranscript.ps1: -------------------------------------------------------------------------------- 1 | function setTranscript { 2 | if ($ManagementGroupId) { 3 | if ($onAzureDevOpsOrGitHubActions -eq $true) { 4 | if ($HierarchyMapOnly -eq $true) { 5 | $script:fileNameTranscript = "AzGovViz_HierarchyMapOnly_$($ManagementGroupId)_Log.txt" 6 | } 7 | elseif ($ManagementGroupsOnly -eq $true) { 8 | $script:fileNameTranscript = "AzGovViz_ManagementGroupsOnly_$($ManagementGroupId)_Log.txt" 9 | } 10 | else { 11 | $script:fileNameTranscript = "AzGovViz_$($ManagementGroupId)_Log.txt" 12 | } 13 | } 14 | else { 15 | if ($HierarchyMapOnly -eq $true) { 16 | $script:fileNameTranscript = "AzGovViz_HierarchyMapOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)_Log.txt" 17 | } 18 | elseif ($ManagementGroupsOnly -eq $true) { 19 | $script:fileNameTranscript = "AzGovViz_ManagementGroupsOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)_Log.txt" 20 | } 21 | else { 22 | $script:fileNameTranscript = "AzGovViz_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)_Log.txt" 23 | } 24 | } 25 | } 26 | else { 27 | if ($onAzureDevOpsOrGitHubActions -eq $true) { 28 | if ($HierarchyMapOnly -eq $true) { 29 | $script:fileNameTranscript = 'AzGovViz_HierarchyMapOnly_Log.txt' 30 | } 31 | elseif ($ManagementGroupsOnly -eq $true) { 32 | $script:fileNameTranscript = 'AzGovViz_ManagementGroupsOnly_Log.txt' 33 | } 34 | else { 35 | $script:fileNameTranscript = 'AzGovViz_Log.txt' 36 | } 37 | } 38 | else { 39 | if ($HierarchyMapOnly -eq $true) { 40 | $script:fileNameTranscript = "AzGovViz_HierarchyMapOnly_$($ProductVersion)_$($fileTimestamp)_Log.txt" 41 | } 42 | elseif ($ManagementGroupsOnly -eq $true) { 43 | $script:fileNameTranscript = "AzGovViz_ManagementGroupsOnly_$($ProductVersion)_$($fileTimestamp)_Log.txt" 44 | } 45 | else { 46 | $script:fileNameTranscript = "AzGovViz_$($ProductVersion)_$($fileTimestamp)_Log.txt" 47 | } 48 | } 49 | } 50 | Write-Host "Writing transcript: $($outputPath)$($DirectorySeparatorChar)$($fileNameTranscript)" 51 | Start-Transcript -Path "$($outputPath)$($DirectorySeparatorChar)$($fileNameTranscript)" 52 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/showMemoryUsage.ps1: -------------------------------------------------------------------------------- 1 | function showMemoryUsage { 2 | 3 | function makeDouble { 4 | [CmdletBinding()] 5 | Param 6 | ( 7 | [Parameter(Mandatory = $true)]$MemoryUsed 8 | ) 9 | 10 | try { 11 | $memoryUsedDouble = [double]($memoryUsed -replace ',', '.') 12 | } 13 | catch { 14 | $memoryUsedDouble = [string]$MemoryUsed 15 | } 16 | return $memoryUsedDouble 17 | } 18 | 19 | function getMemoryUsage { 20 | if ($IsLinux) { 21 | $memoryUsed = 100 - (free | grep Mem | awk '{print $4/$2 * 100.0}') 22 | makeDouble $memoryUsed 23 | } 24 | if ($IsWindows) { 25 | $memoryUsed = (Get-CimInstance win32_operatingsystem | ForEach-Object { '{0:N2}' -f ((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory) * 100) / $_.TotalVisibleMemorySize) }) 26 | makeDouble $memoryUsed 27 | } 28 | } 29 | $memoryUsed = getMemoryUsage 30 | 31 | if ($memoryUsed -is [double]) { 32 | if ($memoryUsed -gt $CriticalMemoryUsage) { 33 | Write-Host "System memory utilization HIGH: $([math]::Round($memoryUsed))%" -ForegroundColor Magenta 34 | Write-Host 'Init garbage collection (GC)' 35 | $PSMemoryBefore = [System.GC]::GetTotalMemory($false) 36 | Write-Host " PS memory used before GC: $($PSMemoryBefore /1MB)MB ($PSMemoryBefore)" 37 | $startGC = Get-Date 38 | $PSMemoryAfter = [System.GC]::GetTotalMemory($true) 39 | $endGC = Get-Date 40 | $PSMemoryDiff = $PSMemoryBefore - $PSMemoryAfter 41 | Write-Host " PS memory used after GC: $($PSMemoryAfter /1MB)MB ($PSMemoryAfter)" 42 | Write-Host " GC cleared $($PSMemoryDiff /1MB)MB ($PSMemoryDiff)" -ForegroundColor Green 43 | Write-Host " GC duration: $((New-TimeSpan -Start $startGC -End $endGC).TotalSeconds) seconds" 44 | Write-Host " System memory utilization after GC: $(getMemoryUsage)%" 45 | } 46 | else { 47 | if ($ShowMemoryUsage) { 48 | Write-Host "System memory utilization: $([math]::Round($memoryUsed))%" 49 | } 50 | } 51 | } 52 | else { 53 | Write-Host "System memory utilization: $($memoryUsed)% (not double)" 54 | } 55 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/testGuid.ps1: -------------------------------------------------------------------------------- 1 | function testGuid { 2 | [OutputType([bool])] 3 | param 4 | ( 5 | [Parameter(Mandatory = $true)] 6 | [string]$StringGuid 7 | ) 8 | 9 | $ObjectGuid = [System.Guid]::empty 10 | return [System.Guid]::TryParse($StringGuid, [System.Management.Automation.PSReference]$ObjectGuid) # Returns True if successfully parsed 11 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/testPowerShellVersion.ps1: -------------------------------------------------------------------------------- 1 | function testPowerShellVersion { 2 | 3 | Write-Host 'Checking PowerShell edition and version' 4 | $requiredPSVersion = '7.0.3' 5 | $splitRequiredPSVersion = $requiredPSVersion.split('.') 6 | $splitRequiredPSVersionMajor = $splitRequiredPSVersion[0] 7 | $splitRequiredPSVersionMinor = $splitRequiredPSVersion[1] 8 | $splitRequiredPSVersionPatch = $splitRequiredPSVersion[2] 9 | 10 | $thisPSVersion = ($PSVersionTable.PSVersion) 11 | $thisPSVersionMajor = ($thisPSVersion).Major 12 | $thisPSVersionMinor = ($thisPSVersion).Minor 13 | $thisPSVersionPatch = ($thisPSVersion).Patch 14 | 15 | $psVersionCheckResult = 'letsCheck' 16 | 17 | if ($PSVersionTable.PSEdition -eq 'Core' -and $thisPSVersionMajor -eq $splitRequiredPSVersionMajor) { 18 | if ($thisPSVersionMinor -gt $splitRequiredPSVersionMinor) { 19 | $psVersionCheckResult = 'passed' 20 | $psVersionCheck = "(Major[$splitRequiredPSVersionMajor]; Minor[$thisPSVersionMinor] gt $($splitRequiredPSVersionMinor))" 21 | } 22 | else { 23 | if ($thisPSVersionPatch -ge $splitRequiredPSVersionPatch) { 24 | $psVersionCheckResult = 'passed' 25 | $psVersionCheck = "(Major[$splitRequiredPSVersionMajor]; Minor[$splitRequiredPSVersionMinor]; Patch[$thisPSVersionPatch] gt $($splitRequiredPSVersionPatch))" 26 | } 27 | else { 28 | $psVersionCheckResult = 'failed' 29 | $psVersionCheck = "(Major[$splitRequiredPSVersionMajor]; Minor[$splitRequiredPSVersionMinor]; Patch[$thisPSVersionPatch] lt $($splitRequiredPSVersionPatch))" 30 | } 31 | } 32 | } 33 | else { 34 | $psVersionCheckResult = 'failed' 35 | $psVersionCheck = "(Major[$splitRequiredPSVersionMajor] ne $($splitRequiredPSVersionMajor))" 36 | } 37 | 38 | if ($psVersionCheckResult -eq 'passed') { 39 | Write-Host " PS check $psVersionCheckResult : $($psVersionCheck); (minimum supported version '$requiredPSVersion')" 40 | Write-Host " PS Edition: $($PSVersionTable.PSEdition); PS Version: $($PSVersionTable.PSVersion)" 41 | Write-Host ' PS Version check succeeded' -ForegroundColor Green 42 | } 43 | else { 44 | Write-Host " PS check $psVersionCheckResult : $($psVersionCheck)" 45 | Write-Host " PS Edition: $($PSVersionTable.PSEdition); PS Version: $($PSVersionTable.PSVersion)" 46 | Write-Host " Parallelization requires Powershell 'Core' version '$($requiredPSVersion)' or higher" 47 | Throw 'Error - check the last console output for details' 48 | } 49 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/validateAccess.ps1: -------------------------------------------------------------------------------- 1 | function validateAccess { 2 | #region validationAccess 3 | #validation / check 'Microsoft Graph API' Access 4 | $permissionCheckResults = @() 5 | if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true -or $azAPICallConf['htParameters'].accountType -eq 'ServicePrincipal' -or $azAPICallConf['htParameters'].accountType -eq 'ManagedService' -or $azAPICallConf['htParameters'].accountType -eq 'ClientAssertion') { 6 | 7 | Write-Host "Checking $($azAPICallConf['htParameters'].accountType) permissions" 8 | 9 | $permissionsCheckFailed = $false 10 | 11 | $currentTask = 'Test MSGraph Users Read permission' 12 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/users?`$count=true&`$top=1" 13 | $method = 'GET' 14 | $res = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -consistencyLevel 'eventual' -validateAccess 15 | if ($res -eq 'failed') { 16 | $permissionCheckResults += "MSGraph API 'Users Read' permission - check FAILED" 17 | $permissionsCheckFailed = $true 18 | } 19 | else { 20 | $permissionCheckResults += "MSGraph API 'Users Read' permission - check PASSED" 21 | } 22 | 23 | $currentTask = 'Test MSGraph Groups Read permission' 24 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/groups?`$count=true&`$top=1" 25 | $method = 'GET' 26 | $res = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -consistencyLevel 'eventual' -validateAccess 27 | if ($res -eq 'failed') { 28 | $permissionCheckResults += "MSGraph API 'Groups Read' permission - check FAILED" 29 | $permissionsCheckFailed = $true 30 | } 31 | else { 32 | $permissionCheckResults += "MSGraph API 'Groups Read' permission - check PASSED" 33 | } 34 | 35 | $currentTask = 'Test MSGraph ServicePrincipals Read permission' 36 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/servicePrincipals?`$count=true&`$top=1" 37 | $method = 'GET' 38 | $res = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -consistencyLevel 'eventual' -validateAccess 39 | if ($res -eq 'failed') { 40 | $permissionCheckResults += "MSGraph API 'ServicePrincipals Read' permission - check FAILED" 41 | $permissionsCheckFailed = $true 42 | } 43 | else { 44 | $permissionCheckResults += "MSGraph API 'ServicePrincipals Read' permission - check PASSED" 45 | } 46 | 47 | if (-not $NoPIMEligibility) { 48 | $currentTask = 'Test MSGraph PrivilegedAccess.Read.AzureResources permission' 49 | $uriExt = "&`$expand=parent&`$filter=(type eq 'subscription' or type eq 'managementgroup')&`$top=1" 50 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/resources?`$select=id,displayName,type,externalId" + $uriExt 51 | $res = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -currentTask $currentTask -validateAccess 52 | if ($res -eq 'failed') { 53 | $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" 54 | $permissionsCheckFailed = $true 55 | } 56 | else { 57 | $permissionCheckResults += "MSGraph API 'PrivilegedAccess.Read.AzureResources' permission - check PASSED" 58 | } 59 | } 60 | } 61 | #endregion validationAccess 62 | 63 | #ManagementGroup helper 64 | #region managementGroupHelper 65 | if (-not $ManagementGroupId) { 66 | #$catchResult = "letscheck" 67 | $currentTask = 'Getting all Management Groups' 68 | #Write-Host $currentTask 69 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups?api-version=2020-05-01" 70 | $method = 'GET' 71 | $getAzManagementGroups = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -validateAccess 72 | 73 | if ($getAzManagementGroups -eq 'failed') { 74 | $permissionCheckResults += "RBAC 'Reader' permissions on Management Group - check FAILED (use Id, not displayName)" 75 | $permissionsCheckFailed = $true 76 | } 77 | else { 78 | $permissionCheckResults += "RBAC 'Reader' permissions on Management Group - check PASSED" 79 | } 80 | 81 | Write-Host 'Permission check results' 82 | foreach ($permissionCheckResult in $permissionCheckResults) { 83 | if ($permissionCheckResult -like '*PASSED*') { 84 | Write-Host $permissionCheckResult -ForegroundColor Green 85 | } 86 | else { 87 | Write-Host $permissionCheckResult -ForegroundColor DarkRed 88 | } 89 | } 90 | if ($permissionsCheckFailed -eq $true) { 91 | Write-Host "Please consult the documentation: https://$($GithubRepository)#required-permissions-in-azure" 92 | Throw 'Error - Azure Governance Visualizer: check the last console output for details' 93 | } 94 | 95 | if ($getAzManagementGroups.Count -eq 0) { 96 | Write-Host 'Management Groups count returned null' 97 | Throw 'Error - Azure Governance Visualizer: check the last console output for details' 98 | } 99 | else { 100 | Write-Host "Detected $($getAzManagementGroups.Count) Management Groups" 101 | } 102 | 103 | [array]$MgtGroupArray = addIndexNumberToArray -array ($getAzManagementGroups) 104 | if (-not $MgtGroupArray) { 105 | 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 106 | Throw 'Error - Azure Governance Visualizer: check the last console output for details' 107 | } 108 | 109 | selectMg 110 | 111 | if ($($MgtGroupArray[$SelectedMG - 1].Name)) { 112 | $script:ManagementGroupId = $($MgtGroupArray[$SelectedMG - 1].name) 113 | $script:ManagementGroupName = $($MgtGroupArray[$SelectedMG - 1].properties.displayName) 114 | } 115 | else { 116 | Write-Host 's.th. unexpected happened' -ForegroundColor Red 117 | return 118 | } 119 | Write-Host "Selected Management Group: #$($SelectedMG) $ManagementGroupName (Id: $ManagementGroupId)" -ForegroundColor Green 120 | Write-Host '_______________________________________' 121 | } 122 | else { 123 | $currentTask = "Checking permissions for ManagementGroup '$ManagementGroupId'" 124 | Write-Host $currentTask 125 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)?api-version=2020-05-01" 126 | $method = 'GET' 127 | $selectedManagementGroupId = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -listenOn 'Content' -validateAccess 128 | 129 | if ($selectedManagementGroupId -eq 'failed') { 130 | $permissionCheckResults += "RBAC 'Reader' permissions on Management Group '$($ManagementGroupId)' - check FAILED (use Id, not displayName)" 131 | $permissionsCheckFailed = $true 132 | } 133 | else { 134 | $permissionCheckResults += "RBAC 'Reader' permissions on Management Group '$($ManagementGroupId)' - check PASSED" 135 | $script:ManagementGroupId = $selectedManagementGroupId.Name 136 | $script:ManagementGroupName = $selectedManagementGroupId.properties.displayName 137 | } 138 | 139 | Write-Host 'Permission check results' 140 | foreach ($permissionCheckResult in $permissionCheckResults) { 141 | if ($permissionCheckResult -like '*PASSED*') { 142 | Write-Host $permissionCheckResult -ForegroundColor Green 143 | } 144 | else { 145 | Write-Host $permissionCheckResult -ForegroundColor DarkRed 146 | } 147 | } 148 | 149 | if ($permissionsCheckFailed -eq $true) { 150 | Write-Host "Please consult the documentation for permission requirements: https://$($GithubRepository)#technical-documentation" 151 | Throw 'Error - Azure Governance Visualizer: check the last console output for details' 152 | } 153 | } 154 | #endregion managementGroupHelper 155 | 156 | if ($azAPICallConf['htParameters'].accountType -eq 'User') { 157 | validateLeastPrivilegeForUser 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /pwsh/dev/functions/validateLeastPrivilegeForUser.ps1: -------------------------------------------------------------------------------- 1 | function validateLeastPrivilegeForUser { 2 | $currentTask = "Validate least priviledge (Azure Resource side) for executing user $($azapicallConf['htParameters'].userObjectId)" 3 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$($azapicallConf['htParameters'].userObjectId)'" 4 | $method = 'GET' 5 | $getRoleAssignmentsForExecutingUserAtManagementGroupId = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -method $method -currentTask $currentTask 6 | $nonReaderRolesAssigned = ($getRoleAssignmentsForExecutingUserAtManagementGroupId.properties.RoleDefinitionId | Sort-Object -Unique).where({ $_ -notlike '*acdd72a7-3385-48ef-bd42-f606fba81ae7' }) 7 | if ($nonReaderRolesAssigned.Count -gt 0) { 8 | Write-Host '* * * LEAST PRIVILEGE ADVICE' -ForegroundColor DarkRed 9 | Write-Host 'The Azure Governance Visualizer script is executed with more permissions than required.' 10 | 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':" 11 | foreach ($nonReaderRoleAssigned in $nonReaderRolesAssigned) { 12 | $currentTask = "Get RBAC Role definition '$nonReaderRoleAssigned'" 13 | $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($nonReaderRoleAssigned)?api-version=2022-04-01" 14 | $method = 'GET' 15 | $getRole = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -method $method -listenOn Content 16 | 17 | if ($getRole.properties.roleName -eq 'owner' -or $getRole.properties.roleName -eq 'contributor') { 18 | Write-Host " - $($getRole.properties.roleName) ($($getRole.properties.type)) !!!" 19 | } 20 | else { 21 | Write-Host " - $($getRole.properties.roleName) ($($getRole.properties.type))" 22 | } 23 | } 24 | Write-Host "The required Azure RBAC role at Management Group scope '$ManagementGroupId' is 'Reader' (acdd72a7-3385-48ef-bd42-f606fba81ae7)." 25 | 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'" 26 | Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor DarkRed 27 | Pause 28 | } 29 | else { 30 | 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 31 | } 32 | } -------------------------------------------------------------------------------- /pwsh/dev/functions/verifyModules3rd.ps1: -------------------------------------------------------------------------------- 1 | function verifyModules3rd { 2 | [CmdletBinding()]Param( 3 | [object]$modules 4 | ) 5 | 6 | foreach ($module in $modules) { 7 | $moduleVersion = $module.ModuleVersion 8 | 9 | if ($moduleVersion) { 10 | Write-Host "Verify '$($module.ModuleName)' version '$moduleVersion'" 11 | } 12 | else { 13 | Write-Host "Verify '$($module.ModuleName)' (latest)" 14 | } 15 | 16 | $maxRetry = 3 17 | $tryCount = 0 18 | do { 19 | $tryCount++ 20 | if ($tryCount -gt $maxRetry) { 21 | Write-Host " Managing '$($module.ModuleName)' failed (tried $($tryCount - 1)x)" 22 | throw " Managing '$($module.ModuleName)' failed" 23 | } 24 | 25 | $installModuleSuccess = $false 26 | try { 27 | if (-not $moduleVersion) { 28 | Write-Host ' Check latest module version' 29 | try { 30 | $moduleVersion = (Find-Module -Name $($module.ModuleName)).Version 31 | Write-Host " $($module.ModuleName) Latest module version: $moduleVersion" 32 | } 33 | catch { 34 | Write-Host " $($module.ModuleName) - Check latest module version failed" 35 | throw " $($module.ModuleName) - Check latest module version failed" 36 | } 37 | } 38 | 39 | if (-not $installModuleSuccess) { 40 | try { 41 | $moduleVersionLoaded = (Get-InstalledModule -Name $($module.ModuleName)).Version 42 | if ([System.Version]$moduleVersionLoaded -eq [System.Version]$moduleVersion) { 43 | $installModuleSuccess = $true 44 | } 45 | else { 46 | Write-Host " $($module.ModuleName) - Deviating module version '$moduleVersionLoaded'" 47 | if ([System.Version]$moduleVersionLoaded -gt [System.Version]$moduleVersion) { 48 | if (($env:SYSTEM_TEAMPROJECTID -and $env:BUILD_REPOSITORY_ID) -or $env:GITHUB_ACTIONS) { 49 | #AzDO or GH 50 | throw " $($module.ModuleName) - Deviating module version $moduleVersionLoaded" 51 | } 52 | else { 53 | Write-Host " Current module version '$moduleVersionLoaded' greater than the minimum required version '$moduleVersion' -> tolerated" -ForegroundColor Yellow 54 | $installModuleSuccess = $true 55 | } 56 | } 57 | else { 58 | Write-Host " Current module version '$moduleVersionLoaded' lower than the minimum required version '$moduleVersion' -> failed" 59 | throw " $($module.ModuleName) - Deviating module version $moduleVersionLoaded" 60 | } 61 | } 62 | } 63 | catch { 64 | throw 65 | } 66 | } 67 | } 68 | catch { 69 | Write-Host " '$($module.ModuleName) $moduleVersion' not installed" 70 | if (($env:SYSTEM_TEAMPROJECTID -and $env:BUILD_REPOSITORY_ID) -or $env:GITHUB_ACTIONS) { 71 | Write-Host " Installing $($module.ModuleName) module ($($moduleVersion))" 72 | $installAzAPICallModuleTryCounter = 0 73 | do { 74 | $installAzAPICallModuleTryCounter++ 75 | try { 76 | $params = @{ 77 | Name = "$($module.ModuleName)" 78 | Force = $true 79 | RequiredVersion = $moduleVersion 80 | ErrorAction = 'Stop' 81 | } 82 | Install-Module @params 83 | $installAzAPICallModuleSuccess = $true 84 | Write-Host " Try#$($installAzAPICallModuleTryCounter) Installing '$($module.ModuleName)' module ($($moduleVersion)) succeeded" 85 | } 86 | catch { 87 | Write-Host " Try#$($installAzAPICallModuleTryCounter) Installing '$($module.ModuleName)' module ($($moduleVersion)) failed - sleep $($installAzAPICallModuleTryCounter) seconds" 88 | Start-Sleep -Seconds $installAzAPICallModuleTryCounter 89 | $installAzAPICallModuleSuccess = $false 90 | } 91 | } 92 | until($installAzAPICallModuleTryCounter -gt 10 -or $installAzAPICallModuleSuccess) 93 | if (-not $installAzAPICallModuleSuccess) { 94 | throw " Installing '$($module.ModuleName)' module ($($moduleVersion)) failed" 95 | } 96 | } 97 | else { 98 | do { 99 | $installModuleUserChoice = $null 100 | $installModuleUserChoice = Read-Host " Do you want to install $($module.ModuleName) module ($($moduleVersion)) from the PowerShell Gallery? (y/n)" 101 | if ($installModuleUserChoice -eq 'y') { 102 | try { 103 | Install-Module -Name $module.ModuleName -RequiredVersion $moduleVersion -Force -ErrorAction Stop 104 | try { 105 | Import-Module -Name $module.ModuleName -RequiredVersion $moduleVersion -Force -ErrorAction Stop 106 | } 107 | catch { 108 | throw " 'Import-Module -Name $($module.ModuleName) -RequiredVersion $moduleVersion -Force' failed" 109 | } 110 | } 111 | catch { 112 | throw " 'Install-Module -Name $($module.ModuleName) -RequiredVersion $moduleVersion' failed" 113 | } 114 | } 115 | elseif ($installModuleUserChoice -eq 'n') { 116 | Write-Host " $($module.ModuleName) module is required, please visit https://aka.ms/$($module.ModuleProductName) or https://www.powershellgallery.com/packages/$($module.ModuleProductName)" 117 | throw " $($module.ModuleName) module is required" 118 | } 119 | else { 120 | Write-Host " Accepted input 'y' or 'n'; start over.." 121 | } 122 | } 123 | until ($installModuleUserChoice -eq 'y') 124 | } 125 | } 126 | } 127 | until ($installModuleSuccess) 128 | Write-Host " Verify '$($module.ModuleName)' version '$moduleVersion' succeeded" -ForegroundColor Green 129 | } 130 | } -------------------------------------------------------------------------------- /setup.md: -------------------------------------------------------------------------------- 1 | # Azure Governance Visualizer (AzGovViz) deployment guide 2 | 3 | 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: 4 | 5 | - Running it ad-hoc from a workstation console or dev container 6 | - Running it from Azure DevOps 7 | - Running it from GitHub 8 | - Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App 9 | 10 | 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. 11 | 12 | ## Prerequisites 13 | 14 | - 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. 15 | - 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).) 16 | 17 | ## Set up and run Azure Governance Visualizer from the console 18 | 19 | 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. 20 | 21 | :arrow_right: Follow the instructions to [Configure and run from the console](./setup/console.md). 22 | 23 | ## Set up and run Azure Governance Visualizer in Azure DevOps 24 | 25 | 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. 26 | 27 | :arrow_right: Follow the instructions to [Configure and run from Azure DevOps](./setup/azure-devops.md). 28 | 29 | ## Set up and run Azure Governance Visualizer in GitHub 30 | 31 | 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. 32 | 33 | :arrow_right: Follow the instructions to [Configure and run from GitHub](./setup/github.md). 34 | 35 | ## Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App 36 | 37 | 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. 38 | 39 | :arrow_right: Follow the instructions to [Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App](./setup/azure-web-app.md). 40 | -------------------------------------------------------------------------------- /setup/azure-web-app.md: -------------------------------------------------------------------------------- 1 | # Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App 2 | 3 | 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. 4 | 5 | You can either setup the azure Web App [manually](#manual-setup) or deploy [code based](#code-based-setup) using the Azure Governance Visualizer accelerator. 6 | 7 | ## Manual setup 8 | 9 | ### Prerequisites 10 | * 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 11 | ![alt text](../img/webapp_create.png "Azure Web App Create") 12 | * Step through the configuration. I typically use the Code for the publish and then select the Runtime stack that you standardize on 13 | ![alt text](../img/webapp_configure.png "Azure Web App Configure") 14 | * No need to configure anything, unless your organization policies require you to do so 15 | NOTE: it is a good practice to tag your resource for operational and finance reasons 16 | * In the webapp _Configuration_ add the name of the HTML output file to the _Default Documents_ 17 | ![alt text](../img/webapp_defaultdocs.png "Azure Web App Default documents") 18 | * Make sure to configure Authentication! 19 | ![alt text](../img/webapp_authentication.png "Azure Web App Authentication") 20 | 21 | ### Azure DevOps 22 | 23 | * Assign the Azure DevOps Service Connection´s Service Principal with RBAC Role __Website Contributor__ on the Azure Web App 24 | * Edit the `.azuredevops/AzGovViz.variables.yml` file 25 | ![alt text](../img/webapp_AzDO_yml.png "Azure DevOps YAML variables") 26 | 27 | ### GitHub Actions 28 | 29 | * Assign the Service Principal used in GitHub with RBAC Role __Website Contributor__ on the Azure Web App 30 | * Edit the `.github/workflows/AzGovViz_OIDC.yml` or `.github/workflows/AzGovViz.yml` file 31 | ![alt text](../img/webapp_GitHub_yml.png "GitHub YAML variables") 32 | 33 | ## Code based setup 34 | Use the [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Governance-Visualizer-Accelerator) to deploy the Azure Web App per code. -------------------------------------------------------------------------------- /setup/console.md: -------------------------------------------------------------------------------- 1 | 2 | # Configure and run Azure Governance Visualizer from the console 3 | 4 | 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. 5 | 6 | ## Prerequisites 7 | 8 | The following must be installed on the workstation that will be used to run the scripts: 9 | 10 | - [Git](https://git-scm.com/downloads) 11 | - [PowerShell 7](https://github.com/PowerShell/PowerShell#get-powershell) (minimum supported version 7.0.3) 12 | - [Azure PowerShell](https://learn.microsoft.com/powershell/azure/install-azure-powershell), specifically `Az.Accounts`. 13 | - [AzAPICall](https://github.com/JulianHayward/AzAPICall#get--set-azapicall-powershell-module) 14 | 15 | > There is a [dev container provided in this repo](#using-github-codespaces-as-your-console) if you'd wish to use GitHub Codespaces. 16 | 17 | ## 1. Validate Microsoft Graph permissions for your user 18 | 19 | :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). 20 | 21 | _- or -_ 22 | 23 | :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. 24 | 25 | _- or -_ 26 | 27 | :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. 28 | 29 | ### Set up to execute as a tenant _guest user_ 30 | 31 | 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). 32 | 33 | :arrow_down_small: Once that is configured, continue with [**2. Validate Azure permissions for your user**](#2-validate-azure-permissions-for-your-user). 34 | 35 | ### Set up to execute as a _service principal_ 36 | 37 | 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: 38 | 39 | - '**Application / Application.Read.All**' 40 | - '**Group / Group.Read.All**' 41 | - '**User / User.Read.All**' 42 | - '**PrivilegedAccess / PrivilegedAccess.Read.AzureResources**' 43 | 44 | #### Assign Microsoft Graph permissions, if needed 45 | 46 | **:computer_mouse: Use the Microsoft Entra admin center to assign permissions to the service principal:** 47 | 48 | > 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. 49 | 50 | 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/). 51 | 1. Click on '**App registrations**' 52 | 1. Search for the existing application (service principal) 53 | 1. Under '**Manage**' click on '**API permissions**' 54 | 1. Click on '**Add a permissions**' 55 | 1. Click on '**Microsoft Graph**' 56 | 1. Click on '**Application permissions**' 57 | 1. Select the following set of permissions and click '**Add permissions**' 58 | - **Application / Application.Read.All** 59 | - **Group / Group.Read.All** 60 | - **User / User.Read.All** 61 | - **PrivilegedAccess / PrivilegedAccess.Read.AzureResources** 62 | 1. Click on 'Add a permissions' 63 | 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**' 64 | - Now you will find the permissions with status '**Granted for _TenantName_**' 65 | 66 | Permissions and admin consent granted in Microsoft Entra ID for the service principal (App Registration): 67 | 68 | ![Permissions in Microsoft Entra ID](../img/aadpermissionsportal_4.jpg) 69 | 70 | ## 2. Validate Azure permissions for your user 71 | 72 | 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**. 73 | 74 | ### Assign Azure permissions, if needed 75 | 76 | 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. 77 | 78 | **:computer_mouse: Use the Azure portal to validate and assign the role:** 79 | 80 | 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. 81 | 82 | **:keyboard: Or use PowerShell to assign the role:** 83 | 84 | ```powershell 85 | $objectId = "" 86 | $managementGroupId = "" 87 | 88 | New-AzRoleAssignment ` 89 | -ObjectId $objectId ` 90 | -RoleDefinitionName "Reader" ` 91 | -Scope /providers/Microsoft.Management/managementGroups/$managementGroupId 92 | ``` 93 | 94 | ## 3. Clone the Azure Governance Visualizer repository 95 | 96 | You'll need a copy of this repository on your workstation. 97 | 98 | ```powershell 99 | git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" 100 | Set-Location "Azure-MG-Sub-Governance-Reporting" 101 | ``` 102 | 103 | ## 4. Authenticate to Azure 104 | 105 | **As your user:** 106 | 107 | ```powershell 108 | Connect-AzAccount -TenantId -UseDeviceAuthentication 109 | ``` 110 | 111 | _- or -_ 112 | 113 | **As the service principal:** 114 | 115 | 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. 116 | 117 | ```powershell 118 | $pscredential = Get-Credential 119 | Connect-AzAccount -ServicePrincipal -TenantId -Credential $pscredential 120 | ``` 121 | 122 | User: Enter '**Application (client) ID**' of the App registration OR '**Application ID**' of the service principal (Enterprise application) 123 | 124 | Password for user \: Enter App registration's secret 125 | 126 | ## 5. Run the Azure Governance Visualizer 127 | 128 | 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) 129 | 130 | ```powershell 131 | .\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -OutputPath "c:\AzGovViz-Output" 132 | ``` 133 | 134 | ## 6. View the results 135 | 136 | Open the generated HTML in your default browser. 137 | 138 | ```powershell 139 | Set-Location -Path "c:\AzGovViz-Output" 140 | Get-ChildItem 141 | Invoke-Item ".\AzGovViz*.html" 142 | ``` 143 | 144 | There is also a markdown version available as well in the output directory. 145 | 146 | ## Using GitHub Codespaces as your console 147 | 148 | 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. 149 | 150 | ### Visual screenshot tour of that experience 151 | 152 | ![Azure Governance Visualizer GitHub Codespaces](../img/codespaces0.png) 153 | 154 | ![Azure Governance Visualizer GitHub Codespaces](../img/codespaces1.png) 155 | 156 | ![Azure Governance Visualizer GitHub Codespaces](../img/codespaces2.png) 157 | 158 | ![Azure Governance Visualizer GitHub Codespaces](../img/codespaces3.png) 159 | 160 | ![Azure Governance Visualizer GitHub Codespaces](../img/codespaces4.png) 161 | 162 | ## Next steps 163 | 164 | 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. 165 | -------------------------------------------------------------------------------- /setup/github.md: -------------------------------------------------------------------------------- 1 | 2 | # Configure and run Azure Governance Visualizer from GitHub 3 | 4 | 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. 5 | 6 | ## Prerequisites 7 | 8 | - A GitHub organization in which you have enough permissions to create a repository. 9 | 10 | ## 1. Create GitHub repository 11 | 12 | 1. Go to to start the repository creation process. 13 | 1. Use '**https:\//github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git**' as the clone URL. 14 | 1. Select your existing GitHub organization. 15 | 1. Select 'Private' 16 | 1. Click on 'Begin import' 17 | 1. Navigate to your newly created repository 18 | 19 | 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. 20 | 21 | ## 2. Create and configure a service principal 22 | 23 | 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. 24 | 25 | There are a few options to create the service principal, both will result in least privilege access: 26 | 27 | - **Option 1** - [Use workload identity federation](#option-1---use-workload-identity-federation-recommended) _(This is the recommended option.)_ 28 | - **Option 2** - [Create and manage a service principal](#option-2---create-and-manage-a-service-principal) 29 | 30 | ### Option 1 - Use workload identity federation (recommended) 31 | 32 | 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. 33 | 34 | 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/) 35 | 1. Click on '**App registrations**' 36 | 1. Click on '**New registration**' 37 | 1. Name your application (e.g. 'AzureGovernanceVisualizer_SC') 38 | 1. Click '**Register**' 39 | 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 40 | 1. Under '**Manage**' click on '**Certificates & Secrets**' 41 | 1. Click on '**Federated credentials**' 42 | 1. Click 'Add credential' 43 | 1. Select Federation credential scenario 'GitHub Actions deploying Azure Resources' 44 | 1. Fill the field 'Organization' with your GitHub Organization name 45 | 1. Fill the field 'Repository' with your GitHub repository name 46 | 1. For the entity type select 'Branch' 47 | 1. Fill the field 'GitHub branch name' with your branch name (default is 'master' if you imported the Azure Governance Visualizer repository) 48 | 1. Fill the field 'Name' with a name (e.g. AzureGovernanceVisualizer_GitHub_Actions) 49 | 1. Click 'Add' 50 | 51 | #### Store the service principal configuration in GitHub 52 | 53 | 1. In the GitHub repository, navigate to 'Settings' 54 | 1. Click on 'Secrets' 55 | 1. Click on 'Actions' 56 | 1. Click 'New repository secret' 57 | 1. Create the following three secrets: 58 | - Name: **CLIENT_ID** 59 | Value: `Application (client) ID (GUID)` 60 | - Name: **TENANT_ID** 61 | Value: `Tenant ID (GUID)` 62 | - Name: **SUBSCRIPTION_ID** 63 | Value: `Subscription ID (GUID)` 64 | 65 | ### Option 2 - Create and manage a service principal 66 | 67 | 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. 68 | 69 | 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/) 70 | 1. Click on '**App registrations**' 71 | 1. Name your application (e.g. 'AzureGovernanceVisualizer_SC') 72 | 1. Click '**Register**' 73 | 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 74 | 1. Under '**Manage**' click on '**Certificates & Secrets**' 75 | 1. Click on '**New client secret**' 76 | 1. Provide a good description and choose the expiry time based on your need and click '**Add**' 77 | 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 78 | 79 | #### Store the newly created credentials in GitHub 80 | 81 | 1. In the GitHub repository, navigate to 'Settings' 82 | 1. Click on 'Secrets' 83 | 1. Click on 'Actions' 84 | 1. Click 'New repository secret' 85 | - Name: **CREDS** 86 | - Value: 87 | 88 | ```json 89 | { 90 | "tenantId": "", 91 | "subscriptionId": "", 92 | "clientId": "", 93 | "clientSecret": "" 94 | } 95 | ``` 96 | 97 | ## 3. Set GitHub workflow permissions 98 | 99 | 1. In the GitHub repository, navigate to 'Settings' 100 | 1. Click on 'Actions' 101 | 1. Click on 'General' 102 | 1. Under 'Workflow permissions' select '**Read and write permissions**' 103 | 1. Click 'Save' 104 | 105 | ## 4. Configure the workflow YAML file 106 | 107 | 1. In the folder `./github/workflows` edit the appropriate YAML file based on your choice in Step 2 108 | - **[AzGovViz_OIDC.yml](../.github/workflows/AzGovViz_OIDC.yml)** for Option 1 (workload identity federation) 109 | - **[AzGovViz.yml](../.github/workflows/AzGovViz.yml)** for Option 2 (Normal service principal) 110 | 1. In the `env` section enter your target Azure management group ID 111 | 1. If you want to continuously run Azure Governance Visualizer then enable the `schedule` in the `on` section 112 | 113 | ## 5. Run Azure Governance Visualizer in GitHub actions 114 | 115 | 1. In the GitHub repository, navigate to 'Actions' 116 | 1. Click 'Enable GitHub Actions on this repository' 117 | 1. Select the configured Azure Governance Visualizer workflow file 118 | 1. Click 'Run workflow' 119 | 120 | ## 6. Publish the Azure Governance Visualizer HTML to a Azure Web App _(Optional)_ 121 | 122 | 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. 123 | 124 | ## Next steps 125 | 126 | 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. 127 | -------------------------------------------------------------------------------- /slides/AzGovViz_intro.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/6a90d07a14166bf6c044741c62837c8e0a2a0256/slides/AzGovViz_intro.pdf -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProductVersion": "6.7.2" 3 | } -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | v6_major_20230330_1 --------------------------------------------------------------------------------