From f5cecbee0bf05dcda915b9ee157dece88b82f1c9 Mon Sep 17 00:00:00 2001 From: quoteee <45695032+JulianHayward@users.noreply.github.com> Date: Mon, 1 Nov 2021 21:00:54 +0100 Subject: [PATCH 1/2] v6_major_20211101_1 --- README.md | 34 +- history.md | 7 + pipeline/AzGovViz.yml | 9 +- pwsh/AzGovVizParallel.ps1 | 849 ++++++++++++++++++++++++-------------- setup.md | 259 ++++++------ 5 files changed, 687 insertions(+), 471 deletions(-) diff --git a/README.md b/README.md index 3823e7be..ba1b57f5 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,13 @@ Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/arch ## Release history +__Changes__ (2021-Nov-01 / Major) + +* New output - Feature request to create __Scope Insights__ output per Subscription has been implement. With this new feature you can share Subscription __Scope Insights__ with Subscription responsible staff. Use parameter `-NoSingleSubscriptionOutput` to disable the feature +* Update [Required permissions in Azure Active Directory](#required-permissions-in-azure-active-directory) for the scenario of a Guest User executing the script +* Add 'daily summary' output (CSV) to easily track your Tenant´s Governance evolution over time - Tim will hopefully create a PR for how he leverages AzGovViz historical data for Azure Log Analytics based dashboards +* Improved permission related error handling + __Changes__ (2021-Oct-25 / Major) * AzAPICall enhanced error handling (general error 'An error has occurred.' ; roleAssignment schedules) @@ -274,7 +281,8 @@ markdown in Azure DevOps Wiki as Code ## AzGovViz Setup Guide -💡 Although 30 minutes of troubleshooting can save you 5 minutes reading the documentation :) .. check the detailed __[Setup Guide](setup.md)__ +💡 Although 30 minutes of troubleshooting can save you 5 minutes reading the documentation :) .. +Check the detailed __[Setup Guide](setup.md)__ ## Technical documentation @@ -314,37 +322,31 @@ This permission is mandatory in each and every scenario! B
Console | Guest user account - Add assignment for the Guest user to AAD Role Directory readers
OR
Use parameters:
 -NoAADGroupsResolveMembers
- 💡 Compare member and guest default permissions + If the tenant is hardened (AAD External Identities / Guest user access = most restrictive) then Guest User must be assigned the AAD Role 'Directory readers'
+ 💡 Compare member and guest default permissions
+ 💡 Restrict Guest permissions C
Console | Service Principal - Option 1 (simple setup but more read permissions than required)
- Add assignment for the Service Principal to AAD Role Directory readers
💡 Directory readers

- Option 2 (explicit permission model) - - + - - -
Feature PermissionsParameter
Get AAD
Guest Users
Get AAD
Users
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / User / User.Read.All
💡 Get user
n/a
Get AAD
Groups
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / Group / Group.Read.All
💡 Get group
NoAADGroupsResolveMembers
Get AAD
SP/App
Service Principal's App registration
grant with Microsoft Graph permissions:
Application permissions / Application / Application.Read.All
💡 Get servicePrincipal, Get application
n/a
@@ -353,30 +355,23 @@ This permission is mandatory in each and every scenario! D
Azure DevOps Pipeline | ServicePrincipal (Service Connection) - Option 1 (simple setup but more read permissions than required)
- Add assignment for the Azure DevOps Service Connection's Service Principal to AAD Role Directory readers
💡 Directory readers

- Option 2 (explicit permission model) - - + - - -
Feature PermissionsParameter
Get AAD
Guest Users
Get AAD
Users
Azure DevOps Service Connection's App registration
grant with Microsoft Graph permissions:
Application permissions / User / User.Read.All
💡 Get user
n/a
Get AAD
Groups
Azure DevOps Service Connection's App registration
grant with Microsoft Graph permissions:
Application permissions / Group / Group.Read.All
💡 Get group
NoAADGroupsResolveMembers
Get AAD
SP/App
Azure DevOps Service Connection's App registration
grant with Microsoft Graph permissions:
Application permissions / Application / Application.Read.All
💡 Get servicePrincipal, Get application
n/a
@@ -441,6 +436,7 @@ Screenshot Azure Portal * `-AADGroupMembersLimit` - Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved * `-NoResources` - Will speed up the processing time but information like Resource diagnostics capability and resource type statistic (featured for large tenants) * `-StatsOptOut` - Opt out sending [stats](#stats) + * `-NoSingleSubscriptionOutput` - Single __Scope Insights__ output per Subscription should not be created ## Integrate with AzOps diff --git a/history.md b/history.md index 50a5f6ae..fdb66476 100644 --- a/history.md +++ b/history.md @@ -4,6 +4,13 @@ ### AzGovViz version 6 +__Changes__ (2021-Nov-01 / Major) + +* New output - Feature request to create __Scope Insights__ output per Subscription has been implement. With this new feature you can share Subscription __Scope Insights__ with Subscription responsible staff. Use parameter `-NoSingleSubscriptionOutput` to disable the feature +* Update [Required permissions in Azure Active Directory](#required-permissions-in-azure-active-directory) for the scenario of a Guest User executing the script +* Add 'daily summary' output (CSV) to easily track your Tenant´s Governance evolution over time - Tim will hopefully create a PR for how he leverages AzGovViz historical data for Azure Log Analytics based dashboards +* Improved permission related error handling + __Changes__ (2021-Oct-25 / Major) * AzAPICall enhanced error handling (general error 'An error has occurred.' ; roleAssignment schedules) diff --git a/pipeline/AzGovViz.yml b/pipeline/AzGovViz.yml index 3e442db0..e7189f06 100644 --- a/pipeline/AzGovViz.yml +++ b/pipeline/AzGovViz.yml @@ -1,11 +1,11 @@ -# AzGovViz v6_major_20211018_1 +# AzGovViz v6_major_20211101_1 # First things first: -# 1. edit line 59 and line 60 -# 2. check line 74 and 85 if branch 'master' is applicable +# 1. edit line 60 and line 61 +# 2. check line 75 and 86 if branch 'master' is applicable # Documentation: https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting # Also check https://www.azadvertizer.net - AzAdvertizer helps you to keep up with the pace by providing overview and insights on new releases and changes/updates for Azure Governance capabilities such as Azure Policy's policy definitions, initiatives (set definitions), aliases and Azure RBAC's role definitions and resource provider operations. # -# Parameters reference (use in line 108) +# Parameters reference (use in line 109) # LimitCriticalPercentage | default is '80' | example: -LimitCriticalPercentage 90 | WhatDoesItDo? marks capabilities that approch limits e.g. limit 100, usage 80 will mark with warning # SubscriptionQuotaIdWhitelist | default is 'undefined' | example: -SubscriptionQuotaIdWhitelist MSDN_, EnterpriseAgreement_ | WhatDoesItDo? processes only Subscriptions that startWith the given QuotaIds # HierarchyMapOnly | switch | example: -HierarchyMapOnly | WhatDoesItDo? only creates the Hierarchy Tree @@ -40,6 +40,7 @@ # AADGroupMembersLimit | example: -AADGroupMembersLimit 333 | WhatDoesItDo? Defines the limit (default=500) of AAD Group members; For AAD Groups that have more members than the defined limit Group members will not be resolved # NoResources | example: -NoResources | WhatDoesItDo? Will speed up the processing time but information like Resource diagnostics capability and resource type stats (featured for large tenants) # StatsOptOut | example: -StatsOptOut | WhatDoesItDo? Will opt-out sending stats +# NoSingleSubscriptionOutput | example: -NoSingleSubscriptionOutput | WhatDoesItDo? Single Scope Insights output per Subscription should not be created trigger: none diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 32a7bbc8..97f8f882 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -129,6 +129,9 @@ .PARAMETER StatsOptOut Will opt-out sending stats +.PARAMETER NoSingleSubscriptionOutput + Single Scope Insights output per Subscription should not be created + .EXAMPLE Define the ManagementGroup ID PS C:\> .\AzGovVizParallel.ps1 -ManagementGroupId @@ -244,7 +247,10 @@ Will opt-out sending stats PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -StatsOptOut -.NOTES + Will not create a single Scope Insights output per Subscription + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoSingleSubscriptionOutput + + .NOTES AUTHOR: Julian Hayward - Customer Engineer - Customer Success Unit | Azure Infrastucture/Automation/Devops/Governance | Microsoft .LINK @@ -257,7 +263,7 @@ Param ( [string]$Product = "AzGovViz", - [string]$ProductVersion = "v6_major_20211025_1", + [string]$ProductVersion = "v6_major_20211101_1", [string]$GithubRepository = "aka.ms/AzGovViz", [string]$ManagementGroupId, [switch]$AzureDevOpsWikiAsCode, #Use this parameter only when running AzGovViz in a Azure DevOps Pipeline! @@ -299,6 +305,7 @@ Param [switch]$RBACAtScopeOnly, [switch]$NoResources, [switch]$StatsOptOut, + [switch]$NoSingleSubscriptionOutput, #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits [int]$LimitRBACCustomRoleDefinitionsTenant = 5000, @@ -992,7 +999,7 @@ $funcResolveObjectIds = $function:ResolveObjectIds.ToString() #API #region azapicall -function AzAPICall($uri, $method, $currentTask, $body, $listenOn, $getConsumption, $getGroup, $getGroupMembersCount, $getApp, $getSP, $getGuests, $caller, $consistencyLevel, $getCount, $getPolicyCompliance, $getMgAscSecureScore, $getRoleAssignmentSchedules, $getDiagnosticSettingsMg, $validateAccess) { +function AzAPICall($uri, $method, $currentTask, $body, $listenOn, $getConsumption, $getGroup, $getGroupMembersCount, $getApp, $caller, $consistencyLevel, $getCount, $getPolicyCompliance, $getMgAscSecureScore, $getRoleAssignmentSchedules, $getDiagnosticSettingsMg, $validateAccess) { $tryCounter = 0 $tryCounterUnexpectedError = 0 $retryAuthorizationFailed = 5 @@ -1136,13 +1143,10 @@ function AzAPICall($uri, $method, $currentTask, $body, $listenOn, $getConsumptio ($getConsumption -and $catchResult.error.code -eq "IndirectCostDisabled") ) -or $catchResult.error.message -like "*The offer MS-AZR-0110P is not supported*" -or - ($getSP -and $catchResult.error.code -like "*Request_ResourceNotFound*") -or - ($getSP -and $catchResult.error.code -like "*Authorization_RequestDenied*") -or ($getApp -and $catchResult.error.code -like "*Request_ResourceNotFound*") -or ($getApp -and $catchResult.error.code -like "*Authorization_RequestDenied*") -or ($getGroup -and $catchResult.error.code -like "*Request_ResourceNotFound*") -or ($getGroupMembersCount -and $catchResult.error.code -like "*Request_ResourceNotFound*") -or - ($getGuests -and $catchResult.error.code -like "*Authorization_RequestDenied*") -or $catchResult.error.code -like "*UnknownError*" -or $catchResult.error.code -like "*BlueprintNotFound*" -or $catchResult.error.code -eq "500" -or @@ -1269,7 +1273,7 @@ function AzAPICall($uri, $method, $currentTask, $body, $listenOn, $getConsumptio Write-Host " $currentTask - try #$tryCounter; returned: (StatusCode: '$($azAPIRequest.StatusCode)') <.code: '$($catchResult.code)'> <.error.code: '$($catchResult.error.code)'> | <.message: '$($catchResult.message)'> <.error.message: '$($catchResult.error.message)'> - (plain : $catchResult) uncertain Group status - skipping for now :)" return "Request_ResourceNotFound" } - if (($getApp -or $getSP) -and $catchResult.error.code -like "*Request_ResourceNotFound*") { + if ($getApp -and $catchResult.error.code -like "*Request_ResourceNotFound*") { Write-Host " $currentTask - try #$tryCounter; returned: (StatusCode: '$($azAPIRequest.StatusCode)') <.code: '$($catchResult.code)'> <.error.code: '$($catchResult.error.code)'> | <.message: '$($catchResult.message)'> <.error.message: '$($catchResult.error.message)'> - (plain : $catchResult) uncertain ServicePrincipal status - skipping for now :)" return "Request_ResourceNotFound" } @@ -1277,21 +1281,10 @@ function AzAPICall($uri, $method, $currentTask, $body, $listenOn, $getConsumptio Write-Host " $currentTask - try #$tryCounter; returned: (StatusCode: '$($azAPIRequest.StatusCode)') <.code: '$($catchResult.code)'> <.error.code: '$($catchResult.error.code)'> | <.message: '$($catchResult.message)'> <.error.message: '$($catchResult.error.message)'> - (plain : $catchResult) cannot get the executing user´s userType information (member/guest) - proceeding as 'unknown'" return "unknown" } - if ((($getApp -or $getSP) -and $catchResult.error.code -like "*Authorization_RequestDenied*") -or ($getGuests -and $catchResult.error.code -like "*Authorization_RequestDenied*")) { - if ($userType -eq "Guest" -or $userType -eq "unknown") { - Write-Host " $currentTask - try #$tryCounter; returned: (StatusCode: '$($azAPIRequest.StatusCode)') <.code: '$($catchResult.code)'> <.error.code: '$($catchResult.error.code)'> | <.message: '$($catchResult.message)'> <.error.message: '$($catchResult.error.message)'> - (plain : $catchResult)" - if ($userType -eq "Guest") { - Write-Host " AzGovViz says: Your UserType is 'Guest' (member/guest/unknown) in the tenant therefore not enough permissions. You have the following options: [1. request membership to AAD Role 'Directory readers'.] Grant explicit Microsoft Graph API permission." -ForegroundColor Yellow - } - if ($userType -eq "unknown") { - Write-Host " AzGovViz says: Your UserType is 'unknown' (member/guest/unknown) in the tenant. Seems you do not have enough permissions geeting AAD related data. You have the following options: [1. request membership to AAD Role 'Directory readers'.]" -ForegroundColor Yellow - } - if ($htParameters.AzureDevOpsWikiAsCode -eq $true) { - Write-Error "Error" - } - else { - Throw "Authorization_RequestDenied" - } + if ($getApp -and $catchResult.error.code -like "*Authorization_RequestDenied*") { + if ($htParameters.userType -eq "Guest") { + Write-Host " $currentTask - try #$tryCounter; returned: (StatusCode: '$($azAPIRequest.StatusCode)') <.code: '$($catchResult.code)'> <.error.code: '$($catchResult.error.code)'> | <.message: '$($catchResult.message)'> <.error.message: '$($catchResult.error.message)'> - (plain : $catchResult) - skip Application (Secrets & Certificates)" + return "skipApplications" } else { Write-Host "- - - - - - - - - - - - - - - - - - - - " @@ -1378,6 +1371,14 @@ function AzAPICall($uri, $method, $currentTask, $body, $listenOn, $getConsumptio return "failed" } + if ($htParameters.userType -eq "Guest" -and $catchResult.error.code -eq "Authorization_RequestDenied") { + #https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions + Write-Host " $currentTask - try #$tryCounter; returned: (StatusCode: '$($azAPIRequest.StatusCode)') '$($catchResult.error.code)' | '$($catchResult.error.message)' - exit" + Write-Host "Tenant seems hardened (AAD External Identities / Guest user access = most restrictive) -> https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions" + Write-Host "AAD Role 'Directory readers' is required for your Guest User Account!" + Throw "Error - AzGovViz: check the last console output for details" + } + } else { if (-not $catchResult.code -and -not $catchResult.error.code -and -not $catchResult.message -and -not $catchResult.error.message -and -not $catchResult -and $tryCounter -lt 6) { @@ -1946,9 +1947,6 @@ function dataCollection($mgId) { $allManagementGroupsFromEntitiesChildOfRequestedMg = $arrayEntitiesFromAPI.where( { $_.type -eq "Microsoft.Management/managementGroups" -and ($_.Name -eq $mgId -or $_.properties.parentNameChain -contains $mgId) }) $allManagementGroupsFromEntitiesChildOfRequestedMgCount = ($allManagementGroupsFromEntitiesChildOfRequestedMg | Measure-Object).Count - #test - Write-Host " BuiltIn PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq "BuiltIn"}).Count) | $((($htCacheDefinitionsPolicy).Values | Where-Object {$_.Type -eq "BuiltIn"}).Count)" - $allManagementGroupsFromEntitiesChildOfRequestedMg | ForEach-Object -Parallel { $mgdetail = $_ #region UsingVARs @@ -2007,7 +2005,6 @@ function dataCollection($mgId) { $function:namingValidation = $using:funcNamingValidation $function:ResolveObjectIds = $using:funcResolveObjectIds #endregion usingVARS - #test $builtInPolicyDefinitionsCount = $using:builtInPolicyDefinitionsCount $addRowToTableDone = $false @@ -2249,7 +2246,7 @@ function dataCollection($mgId) { foreach ($mgPolicyDefinition in $mgPolicyDefinitions) { $hlpMgPolicyDefinitionId = ($mgPolicyDefinition.id).ToLower() - if ($hlpMgPolicyDefinitionId -notlike "/providers/Microsoft.Management/managementGroups/*"){ + if ($hlpMgPolicyDefinitionId -notlike "/providers/Microsoft.Management/managementGroups/*") { Write-Host "!!!** showed up in the foreach loop for custom policy definitions: '$hlpMgPolicyDefinitionId'" } @@ -3139,10 +3136,7 @@ function dataCollection($mgId) { Write-Host " CustomDataCollection ManagementGroups processing duration: $((NEW-TIMESPAN -Start $startMgLoop -End $endMgLoop).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startMgLoop -End $endMgLoop).TotalSeconds) seconds)" #test - Write-Host " BuiltIn PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq "BuiltIn"}).Count) | $((($htCacheDefinitionsPolicy).Values | Where-Object {$_.Type -eq "BuiltIn"}).Count)" - Write-Host " Custom PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq "Custom"}).Count)" - Write-Host " All PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.Count)" - if ($builtInPolicyDefinitionsCount -ne ($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq "BuiltIn"}).Count) -or $builtInPolicyDefinitionsCount -ne ((($htCacheDefinitionsPolicy).Values | Where-Object {$_.Type -eq "BuiltIn"}).Count)){ + if ($builtInPolicyDefinitionsCount -ne ($($htCacheDefinitionsPolicy).Values.where({ $_.Type -eq "BuiltIn" }).Count) -or $builtInPolicyDefinitionsCount -ne ((($htCacheDefinitionsPolicy).Values | Where-Object { $_.Type -eq "BuiltIn" }).Count)) { Write-Host "$builtInPolicyDefinitionsCount -ne $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq "BuiltIn"}).Count) OR $builtInPolicyDefinitionsCount -ne $((($htCacheDefinitionsPolicy).Values | Where-Object {$_.Type -eq "BuiltIn"}).Count)" Write-Host "Listing all PolicyDefinitions:" foreach ($tmpPolicyDefinitionId in ($($htCacheDefinitionsPolicy).Keys | Sort-Object)) { @@ -5046,63 +5040,65 @@ function tableMgHTML($mgChild, $mgChildOf) { $mgLevel = $mgDetails.Level $mgId = $mgDetails.MgId - if ($mgId -eq $defaultManagementGroupId) { - $classDefaultMG = "defaultMG" - } - else { - $classDefaultMG = "" - } + if (-not $NoScopeInsights) { + if ($mgId -eq $defaultManagementGroupId) { + $classDefaultMG = "defaultMG" + } + else { + $classDefaultMG = "" + } - switch ($mgLevel) { - "0" { $levelSpacing = "|  " } - "1" { $levelSpacing = "| - " } - "2" { $levelSpacing = "| - - " } - "3" { $levelSpacing = "| - - - " } - "4" { $levelSpacing = "| - - - - " } - "5" { $levelSpacing = "|- - - - - " } - "6" { $levelSpacing = "|- - - - - - " } - } + switch ($mgLevel) { + "0" { $levelSpacing = "|  " } + "1" { $levelSpacing = "| - " } + "2" { $levelSpacing = "| - - " } + "3" { $levelSpacing = "| - - - " } + "4" { $levelSpacing = "| - - - - " } + "5" { $levelSpacing = "|- - - - - " } + "6" { $levelSpacing = "|- - - - - - " } + } - $mgPath = $htManagementGroupsMgPath.($mgChild).pathDelimited + $mgPath = $htManagementGroupsMgPath.($mgChild).pathDelimited - $mgLinkedSubsCount = ((($optimizedTableForPathQuery | Where-Object { $_.MgId -eq $mgChild -and -not [String]::IsNullOrEmpty($_.SubscriptionId) }).SubscriptionId | Get-Unique) | measure-object).count - $subscriptionsOutOfScopelinkedCount = ($outOfScopeSubscriptions | Where-Object { $_.ManagementGroupId -eq $mgChild } | Measure-Object).count - if ($mgLinkedSubsCount -gt 0 -and $subscriptionsOutOfScopelinkedCount -eq 0) { - $subInfo = "$mgLinkedSubsCount" - } - if ($mgLinkedSubsCount -gt 0 -and $subscriptionsOutOfScopelinkedCount -gt 0) { - $subInfo = "$mgLinkedSubsCount $subscriptionsOutOfScopelinkedCount" - } - if ($mgLinkedSubsCount -eq 0 -and $subscriptionsOutOfScopelinkedCount -gt 0) { - $subInfo = "$subscriptionsOutOfScopelinkedCount" - } - if ($mgLinkedSubsCount -eq 0 -and $subscriptionsOutOfScopelinkedCount -eq 0) { - $subInfo = "" - } + $mgLinkedSubsCount = ((($optimizedTableForPathQuery | Where-Object { $_.MgId -eq $mgChild -and -not [String]::IsNullOrEmpty($_.SubscriptionId) }).SubscriptionId | Get-Unique) | measure-object).count + $subscriptionsOutOfScopelinkedCount = ($outOfScopeSubscriptions | Where-Object { $_.ManagementGroupId -eq $mgChild } | Measure-Object).count + if ($mgLinkedSubsCount -gt 0 -and $subscriptionsOutOfScopelinkedCount -eq 0) { + $subInfo = "$mgLinkedSubsCount" + } + if ($mgLinkedSubsCount -gt 0 -and $subscriptionsOutOfScopelinkedCount -gt 0) { + $subInfo = "$mgLinkedSubsCount $subscriptionsOutOfScopelinkedCount" + } + if ($mgLinkedSubsCount -eq 0 -and $subscriptionsOutOfScopelinkedCount -gt 0) { + $subInfo = "$subscriptionsOutOfScopelinkedCount" + } + if ($mgLinkedSubsCount -eq 0 -and $subscriptionsOutOfScopelinkedCount -eq 0) { + $subInfo = "" + } - if ($mgName -eq $mgId) { - $mgNameAndOrId = "$($mgName -replace "<", "<" -replace ">", ">")" - } - else { - $mgNameAndOrId = "$($mgName -replace "<", "<" -replace ">", ">") ($mgId)" - } + if ($mgName -eq $mgId) { + $mgNameAndOrId = "$($mgName -replace "<", "<" -replace ">", ">")" + } + else { + $mgNameAndOrId = "$($mgName -replace "<", "<" -replace ">", ">") ($mgId)" + } - $script:html += @" + $script:html += @"
"@ - if ($mgId -eq $defaultManagementGroupId) { - $script:html += @" + if ($mgId -eq $defaultManagementGroupId) { + $script:html += @" "@ - } - $script:html += @" + } + $script:html += @" "@ + } tableMgSubDetailsHTML -mgOrSub "mg" -mgchild $mgId tableSubForMgHTML -mgChild $mgId #$childMgs = ($optimizedTableForPathQueryMg | Where-Object { $_.mgParentId -eq "$mgId" }).MgId | sort-object -Unique @@ -5129,72 +5125,86 @@ function tableSubForMgHTML($mgChild) { $subscriptionsOutOfScopelinkedInfo = "" } Write-Host " Building ScopeInsights MG '$mgChild', $subscriptionLinkedCount Subscriptions" + if ($subscriptionLinkedCount -gt 0) { - $script:html += @" + if (-not $NoScopeInsights) { + $script:html += @" + + + + - + + "@) } + + [void]$htmlScopeInsights.AppendLine(@" + +"@) #endregion ScopeInsightsRoleAssignments + + if (-not $NoScopeInsights) { + $script:html += $htmlScopeInsights + } - $script:html += $htmlScopeInsights + if (-not $NoSingleSubscriptionOutput) { + if ($mgOrSub -eq "sub") { + $htmlThisSubSingleOutput = $htmlSubscriptionOnlyStart + $htmlThisSubSingleOutput += $htmlScopeInsights + $htmlThisSubSingleOutput += $htmlSubscriptionOnlyEnd + $htmlThisSubSingleOutput | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($HTMLPath)$($DirectorySeparatorChar)$($fileName)_$($subscriptionId).html" -Encoding utf8 -Force + $htmlThisSubSingleOutput = $null + } + } - if ($scopescnter % 50 -eq 0) { - $script:scopescnter = 0 - Write-Host " append file duration: "(Measure-Command { $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force }).TotalSeconds "seconds" - $script:html = $null - #[System.GC]::Collect() + if (-not $NoScopeInsights) { + if ($scopescnter % 50 -eq 0) { + $script:scopescnter = 0 + Write-Host " append file duration: "(Measure-Command { $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force }).TotalSeconds "seconds" + $script:html = $null + #[System.GC]::Collect() + } } } @@ -9076,14 +9106,12 @@ extensions: [{ name: 'sort' }] $arrayCustomPoliciesOrphanedFinal = [System.Collections.ArrayList]@() foreach ($customPolicyOrphaned in $customPoliciesOrphaned) { - #test if ($customPolicyOrphaned.Id) { if (-not $htPoliciesUsedInPolicySets.($customPolicyOrphaned.Id)) { $null = $arrayCustomPoliciesOrphanedFinal.Add($customPolicyOrphaned) } } else { - #test Write-Host "!!!!!!!!!!!!!!!!!!!!! no Id" $customPolicyOrphaned Write-Host "## all:" @@ -13547,7 +13575,7 @@ extensions: [{ name: 'sort' }] #region SUMMARYResources $startSUMMARYResources = get-date Write-Host " processing TenantSummary Subscriptions Resources" - if (($resourcesAll | Measure-Object).count -gt 0) { + if (($resourcesAll).count -gt 0) { $resourcesAllGroupedByType = $resourcesAll | Select-Object -Property type, count_ | Group-Object type $resourcesTotal = ($resourcesAll.count_ | Measure-Object -Sum).Sum $resourcesResourceTypeCount = ($resourcesAll.type | sort-object -Unique).Count @@ -13572,6 +13600,7 @@ extensions: [{ name: 'sort' }] $htmlSUMMARYResources = $null $htmlSUMMARYResources = foreach ($resourceAllSummarized in $resourcesAllGroupedByType) { $type = $resourceAllSummarized.Name + $script:htDailySummary."ResourceType_$($resourceAllSummarized.Name)" = ($resourceAllSummarized.group.count_ | Measure-Object -Sum).Sum @" @@ -16315,7 +16344,7 @@ tf.init(); $policyAssignmentsPolicyDefinitionId = ($assignmentInfo.properties.policyDefinitionId).ToLower() $policyAssignmentspolicyDefinitionIdGuid = $policyAssignmentsPolicyDefinitionId -replace ".*/" - if ($policyAssignmentsPolicyVariant4ht -eq "policy"){ + if ($policyAssignmentsPolicyVariant4ht -eq "policy") { if (($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId)) { $definitionInfo = ($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId) } @@ -16323,7 +16352,7 @@ tf.init(); $definitionInfo = "unknown" } } - if ($policyAssignmentsPolicyVariant4ht -eq "policySet"){ + if ($policyAssignmentsPolicyVariant4ht -eq "policySet") { if (($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId)) { $definitionInfo = ($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId) } @@ -16337,7 +16366,7 @@ tf.init(); $policyAssignmentsPolicyDefinitionId = ($assignmentInfo.properties.policyDefinitionId).ToLower() $policyAssignmentspolicyDefinitionIdGuid = $policyAssignmentsPolicyDefinitionId -replace ".*/" - if ($policyAssignmentsPolicyVariant4ht -eq "policy"){ + if ($policyAssignmentsPolicyVariant4ht -eq "policy") { if (($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId)) { $definitionInfo = ($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId) } @@ -16345,7 +16374,7 @@ tf.init(); $definitionInfo = "unknown" } } - if ($policyAssignmentsPolicyVariant4ht -eq "policySet"){ + if ($policyAssignmentsPolicyVariant4ht -eq "policySet") { if (($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId)) { $definitionInfo = ($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId) } @@ -16368,7 +16397,7 @@ tf.init(); $policyAssignmentsPolicyDefinitionId = ($assignmentInfo.properties.policyDefinitionId).Tolower() $policyAssignmentspolicyDefinitionIdGuid = $policyAssignmentsPolicyDefinitionId -replace ".*/" - if ($policyAssignmentsPolicyVariant4ht -eq "policy"){ + if ($policyAssignmentsPolicyVariant4ht -eq "policy") { if (($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId)) { $definitionInfo = ($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId) } @@ -16376,7 +16405,7 @@ tf.init(); $definitionInfo = "unknown" } } - if ($policyAssignmentsPolicyVariant4ht -eq "policySet"){ + if ($policyAssignmentsPolicyVariant4ht -eq "policySet") { if (($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId)) { $definitionInfo = ($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId) } @@ -16503,26 +16532,27 @@ tf.init(); #endregion AADSPManagedIdentity #region AADSPCredExpiry - $startAADSPCredExpiryLoop = get-date - Write-Host " processing TenantSummary AAD SP Apps CredExpiry" + if (-not $skipApplications) { + $startAADSPCredExpiryLoop = get-date + Write-Host " processing TenantSummary AAD SP Apps CredExpiry" - $servicePrincipalsOfTypeApplicationCount = ($servicePrincipalsOfTypeApplication | Measure-Object).Count + $servicePrincipalsOfTypeApplicationCount = ($servicePrincipalsOfTypeApplication | Measure-Object).Count - if ($servicePrincipalsOfTypeApplicationCount -gt 0) { - $tfCount = $servicePrincipalsOfTypeApplicationCount - $htmlTableId = "TenantSummary_AADSPCredExpiry" + if ($servicePrincipalsOfTypeApplicationCount -gt 0) { + $tfCount = $servicePrincipalsOfTypeApplicationCount + $htmlTableId = "TenantSummary_AADSPCredExpiry" - $servicePrincipalsOfTypeApplicationSecretsExpiring = $servicePrincipalsOfTypeApplication | Where-Object { $htAppDetails.($_).appPasswordCredentialsGracePeriodExpiryCount -gt 0 } - $servicePrincipalsOfTypeApplicationSecretsExpiringCount = ($servicePrincipalsOfTypeApplicationSecretsExpiring | Measure-Object).Count - $servicePrincipalsOfTypeApplicationCertificatesExpiring = $servicePrincipalsOfTypeApplication | Where-Object { $htAppDetails.($_).appKeyCredentialsGracePeriodExpiryCount -gt 0 } - $servicePrincipalsOfTypeApplicationCertificatesExpiringCount = ($servicePrincipalsOfTypeApplicationCertificatesExpiring | Measure-Object).Count - if ($servicePrincipalsOfTypeApplicationSecretsExpiringCount -gt 0 -or $servicePrincipalsOfTypeApplicationCertificatesExpiringCount -gt 0) { - $warningOrNot = "" - } - else { - $warningOrNot = "" - } - [void]$htmlTenantSummary.AppendLine(@" + $servicePrincipalsOfTypeApplicationSecretsExpiring = $servicePrincipalsOfTypeApplication | Where-Object { $htAppDetails.($_).appPasswordCredentialsGracePeriodExpiryCount -gt 0 } + $servicePrincipalsOfTypeApplicationSecretsExpiringCount = ($servicePrincipalsOfTypeApplicationSecretsExpiring | Measure-Object).Count + $servicePrincipalsOfTypeApplicationCertificatesExpiring = $servicePrincipalsOfTypeApplication | Where-Object { $htAppDetails.($_).appKeyCredentialsGracePeriodExpiryCount -gt 0 } + $servicePrincipalsOfTypeApplicationCertificatesExpiringCount = ($servicePrincipalsOfTypeApplicationCertificatesExpiring | Measure-Object).Count + if ($servicePrincipalsOfTypeApplicationSecretsExpiringCount -gt 0 -or $servicePrincipalsOfTypeApplicationCertificatesExpiringCount -gt 0) { + $warningOrNot = "" + } + else { + $warningOrNot = "" + } + [void]$htmlTenantSummary.AppendLine(@"
Download CSV semicolon | comma @@ -16548,9 +16578,9 @@ tf.init();
"@) - $htmlSUMMARYAADSPCredExpiry = $null - $htmlSUMMARYAADSPCredExpiry = foreach ($serviceprincipalApp in $servicePrincipalsOfTypeApplication | Sort-Object) { - @" + $htmlSUMMARYAADSPCredExpiry = $null + $htmlSUMMARYAADSPCredExpiry = foreach ($serviceprincipalApp in $servicePrincipalsOfTypeApplication | Sort-Object) { + @" @@ -16558,50 +16588,50 @@ tf.init(); "@ - if ($htAppDetails.$serviceprincipalApp.appPasswordCredentialsCount) { - @" + if ($htAppDetails.$serviceprincipalApp.appPasswordCredentialsCount) { + @" "@ - } - else { - @" + } + else { + @" "@ - } + } - if ($htAppDetails.$serviceprincipalApp.appKeyCredentialsCount) { - @" + if ($htAppDetails.$serviceprincipalApp.appKeyCredentialsCount) { + @" "@ - } - else { - @" + } + else { + @" "@ - } + } - @" + @" "@ - } - [void]$htmlTenantSummary.AppendLine($htmlSUMMARYAADSPCredExpiry) - [void]$htmlTenantSummary.AppendLine(@" + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYAADSPCredExpiry) + [void]$htmlTenantSummary.AppendLine(@"

Highlight Management Group in HierarchyMap

Default Management Group docs

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

Management Group Id: $mgId

Management Group Path: $mgPath

"@ + } foreach ($subEntry in $subscriptions | sort-object -Property subscription, subscriptionId) { - $subPath = $htSubscriptionsMgPath.($subEntry.subscriptionId).pathDelimited + #$subPath = $htSubscriptionsMgPath.($subEntry.subscriptionId).pathDelimited if ($subscriptionLinkedCount -gt 1) { - $script:html += @" + if (-not $NoScopeInsights) { + $script:html += @"
"@ + } } #exactly 1 else { - $script:html += @" + if (-not $NoScopeInsights) { + $script:html += @" $($subEntry.subscription -replace "<", "<" -replace ">", ">") ($($subEntry.subscriptionId)) "@ + } } - - $script:html += @" - - - - - - - - +

Highlight Subscription in HierarchyMap

Subscription Name: $($subEntry.subscription -replace "<", "<" -replace ">", ">")

Subscription Id: $($subEntry.subscriptionId)

Subscription Path: $subPath

+ if (-not $NoScopeInsights) { + $script:html += @" + + "@ - tableMgSubDetailsHTML -mgOrSub "sub" -subscriptionId $subEntry.subscriptionId - $script:html += @" + } + + tableMgSubDetailsHTML -mgOrSub "sub" -subscriptionId $subEntry.subscriptionId -subscriptionsMgId $mgChild + if (-not $NoScopeInsights) { + $script:html += @"

Highlight Subscription in HierarchyMap

"@ + } if ($subscriptionLinkedCount -gt 1) { - $script:html += @" + if (-not $NoScopeInsights) { + $script:html += @" "@ + } } } - $script:html += @" + if (-not $NoScopeInsights) { + $script:html += @" "@ + } } else { - $script:html += @" + if (-not $NoScopeInsights) { + $script:html += @"

$subscriptionLinkedCount Subscriptions linked $subscriptionsOutOfScopelinkedInfo

"@ + } } - $script:html += @" + if (-not $NoScopeInsights) { + $script:html += @"
"@ + } } #endregion tableSubForMgHTML #rsi #region ScopeInsights -function tableMgSubDetailsHTML($mgOrSub, $mgChild, $subscriptionId) { +function tableMgSubDetailsHTML($mgOrSub, $mgChild, $subscriptionId, $subscriptionsMgId) { $script:scopescnter++ $htmlScopeInsights = $null $htmlScopeInsights = [System.Text.StringBuilder]::new() @@ -5277,6 +5287,7 @@ function tableMgSubDetailsHTML($mgOrSub, $mgChild, $subscriptionId) { #SubscriptionDetails #$subscriptionId = "b2ac7057-8edf-4617-a1f7-5ed6b44ef2c8" #$subscriptionDetailsReleatedQuery = $optimizedTableForPathQuerySub | Where-Object { $_.SubscriptionId -eq $subscriptionId } + $subPath = $htSubscriptionsMgPath.($subscriptionId).pathDelimited $subscriptionDetailsReleatedQuery = $htSubDetails.($subscriptionId).details $subscriptionState = ($subscriptionDetailsReleatedQuery).SubscriptionState $subscriptionQuotaId = ($subscriptionDetailsReleatedQuery).SubscriptionQuotaId @@ -5312,10 +5323,11 @@ function tableMgSubDetailsHTML($mgOrSub, $mgChild, $subscriptionId) { #endregion ScopeInsightsBaseCollection if ($mgOrSub -eq "sub") { - [void]$htmlScopeInsights.AppendLine(@" -

State: $subscriptionState

-

Subscription Name: $($subscriptionDetailsReleatedQuery.subscription -replace "<", "<" -replace ">", ">")

Subscription Id: $($subscriptionDetailsReleatedQuery.subscriptionId)

Subscription Path: $subPath

State: $subscriptionState

QuotaId: $subscriptionQuotaId

ASC Secure Score: $subscriptionASCPoints Video , Blog , docs

@@ -5796,17 +5808,17 @@ tf.init();}}

No Consumption data available

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

No Consumption data available as parameter -NoAzureConsumption was applied

"@) } + + [void]$htmlScopeInsights.AppendLine(@" +
+"@) #endregion ScopeInsightsConsumptionSub #$endScopeInsightsConsumptionSub = get-date #Write-Host " **ScopeInsightsConsumptionSub data duration: $((NEW-TIMESPAN -Start $startScopeInsightsConsumptionSub -End $endScopeInsightsConsumptionSub).TotalSeconds) seconds" @@ -7999,18 +8011,36 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { else { [void]$htmlScopeInsights.AppendLine(@"

$(($rbacAll | measure-object).count) Role assignments

-
$($type)
$($htAppDetails.$serviceprincipalApp.spGraphDetails.appId) $($htAppDetails.$serviceprincipalApp.spGraphDetails.displayName)$($htAppDetails.$serviceprincipalApp.spGraphDetails.Id) $($htAppDetails.$serviceprincipalApp.appGraphDetails.Id)$($htAppDetails.$serviceprincipalApp.appPasswordCredentialsCount) $($htAppDetails.$serviceprincipalApp.appPasswordCredentialsExpiredCount) $($htAppDetails.$serviceprincipalApp.appPasswordCredentialsGracePeriodExpiryCount) $($htAppDetails.$serviceprincipalApp.appPasswordCredentialsExpiryOKCount) $($htAppDetails.$serviceprincipalApp.appPasswordCredentialsExpiryOKMoreThan2YearsCount)0 0 0 0 0$($htAppDetails.$serviceprincipalApp.appKeyCredentialsCount) $($htAppDetails.$serviceprincipalApp.appKeyCredentialsExpiredCount) $($htAppDetails.$serviceprincipalApp.appKeyCredentialsGracePeriodExpiryCount) $($htAppDetails.$serviceprincipalApp.appKeyCredentialsExpiryOKCount) $($htAppDetails.$serviceprincipalApp.appKeyCredentialsExpiryOKMoreThan2YearsCount)0 0 0 0 0
@@ -16609,31 +16639,31 @@ tf.init(); var tfConfig4$htmlTableId = { base_path: 'https://www.azadvertizer.net/azgovvizv4/tablefilter/', rows_counter: true, "@) - if ($tfCount -gt 10) { - $spectrum = "10, $tfCount" - if ($tfCount -gt 50) { - $spectrum = "10, 25, 50, $tfCount" - } - if ($tfCount -gt 100) { - $spectrum = "10, 30, 50, 100, $tfCount" - } - if ($tfCount -gt 500) { - $spectrum = "10, 30, 50, 100, 250, $tfCount" - } - if ($tfCount -gt 1000) { - $spectrum = "10, 30, 50, 100, 250, 500, 750, $tfCount" - } - if ($tfCount -gt 2000) { - $spectrum = "10, 30, 50, 100, 250, 500, 750, 1000, 1500, $tfCount" - } - if ($tfCount -gt 3000) { - $spectrum = "10, 30, 50, 100, 250, 500, 750, 1000, 1500, 3000, $tfCount" - } - [void]$htmlTenantSummary.AppendLine(@" + if ($tfCount -gt 10) { + $spectrum = "10, $tfCount" + if ($tfCount -gt 50) { + $spectrum = "10, 25, 50, $tfCount" + } + if ($tfCount -gt 100) { + $spectrum = "10, 30, 50, 100, $tfCount" + } + if ($tfCount -gt 500) { + $spectrum = "10, 30, 50, 100, 250, $tfCount" + } + if ($tfCount -gt 1000) { + $spectrum = "10, 30, 50, 100, 250, 500, 750, $tfCount" + } + if ($tfCount -gt 2000) { + $spectrum = "10, 30, 50, 100, 250, 500, 750, 1000, 1500, $tfCount" + } + if ($tfCount -gt 3000) { + $spectrum = "10, 30, 50, 100, 250, 500, 750, 1000, 1500, 3000, $tfCount" + } + [void]$htmlTenantSummary.AppendLine(@" paging: {results_per_page: ['Records: ', [$spectrum]]},/*state: {types: ['local_storage'], filters: true, page_number: true, page_length: true, sort: true},*/ "@) - } - [void]$htmlTenantSummary.AppendLine(@" + } + [void]$htmlTenantSummary.AppendLine(@" btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { delay: 1100 }, no_results_message: true, col_types: [ 'caseinsensitivestring', @@ -16658,15 +16688,21 @@ var tf = new TableFilter('$htmlTableId', tfConfig4$htmlTableId); tf.init(); "@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$servicePrincipalsOfTypeApplicationCount AAD ServicePrincipals type=Application

+"@) + } + + $endAADSPCredExpiryLoop = get-date + Write-Host " TenantSummary AAD SP Apps CredExpiry processing duration: $((NEW-TIMESPAN -Start $startAADSPCredExpiryLoop -End $endAADSPCredExpiryLoop).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startAADSPCredExpiryLoop -End $endAADSPCredExpiryLoop).TotalSeconds) seconds)" } else { [void]$htmlTenantSummary.AppendLine(@" -

$servicePrincipalsOfTypeApplicationCount AAD ServicePrincipals type=Application

+

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

"@) } - - $endAADSPCredExpiryLoop = get-date - Write-Host " TenantSummary AAD SP Apps CredExpiry processing duration: $((NEW-TIMESPAN -Start $startAADSPCredExpiryLoop -End $endAADSPCredExpiryLoop).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startAADSPCredExpiryLoop -End $endAADSPCredExpiryLoop).TotalSeconds) seconds)" #endregion AADSPCredExpiry #region AADSPExternalSP @@ -19718,17 +19754,40 @@ createBearerToken -targetEndPoint "MSGraphAPI" $arrayAPICallTracking = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) $arrayAPICallTrackingCustomDataCollection = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + + + #region validationAccess #validation / check 'Microsoft Graph API' Access $permissionCheckResults = @() -#todo -#guest user ?? + +$userType = "n/a" +if ($accountType -eq "User") { + $currentTask = "Checking AAD UserType" + Write-Host $currentTask + $uri = "$(($htAzureEnvironmentRelatedUrls).($checkContext.Environment.Name).MSGraphUrl)/v1.0/me?`$select=userType" + $method = "GET" + $checkUserType = AzAPICall -uri $uri -method $method -listenOn "Content" -currentTask $currentTask + if ($checkUserType -eq "unknown") { + $userType = $checkUserType + } + else { + $userType = $checkUserType.userType + } + Write-Host " AAD UserType: $($userType)" -ForegroundColor Yellow +} +$htParameters.userType = $userType + + if ($htParameters.AzureDevOpsWikiAsCode -eq $true -or $accountType -eq "ServicePrincipal") { - Write-Host "Checking AzDO ServicePrincipal permissions" - + + if ($htParameters.AzureDevOpsWikiAsCode -eq $true -or $accountType -eq "ServicePrincipal") { + Write-Host "Checking ServicePrincipal permissions" + } $permissionsCheckFailed = $false + $currentTask = "Test AAD Users Read permission" $uri = "$(($htAzureEnvironmentRelatedUrls).($checkContext.Environment.Name).MSGraphUrl)/v1.0/users?`$count=true&`$top=1" $method = "GET" @@ -19741,6 +19800,7 @@ if ($htParameters.AzureDevOpsWikiAsCode -eq $true -or $accountType -eq "ServiceP $permissionCheckResults += "AAD Users Read permission check PASSED" } + $currentTask = "Test AAD Groups Read permission" $uri = "$(($htAzureEnvironmentRelatedUrls).($checkContext.Environment.Name).MSGraphUrl)/v1.0/groups?`$count=true&`$top=1" $method = "GET" @@ -19763,7 +19823,7 @@ if ($htParameters.AzureDevOpsWikiAsCode -eq $true -or $accountType -eq "ServiceP } else { $permissionCheckResults += "AAD ServicePrincipals Read permission check PASSED" - } + } } #endregion validationAccess @@ -19924,6 +19984,7 @@ if ($accountType -eq "User") { } #> +<# $userType = "n/a" if ($accountType -eq "User") { $currentTask = "Checking AAD UserType" @@ -19939,6 +20000,7 @@ if ($accountType -eq "User") { } Write-Host " AAD UserType: $($userType)" -ForegroundColor Yellow } +#> $newTable = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) @@ -20238,6 +20300,15 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $paramsUsed += "NoScopeInsights: false " } + if ($NoSingleSubscriptionOutput) { + Write-Host " No single Subscription output will not be created (-NoSingleSubscriptionOutput = $($NoSingleSubscriptionOutput))" -ForegroundColor Green + $paramsUsed += "NoSingleSubscriptionOutput: true " + } + else { + Write-Host " Single Subscription output will be created (-NoSingleSubscriptionOutput = $($NoSingleSubscriptionOutput))" -ForegroundColor Yellow + $paramsUsed += "NoSingleSubscriptionOutput: false " + } + if ($NoResourceProvidersDetailed -eq $true) { Write-Host " ResourceProvider Detailed for TenantSummary disabled (-NoResourceProvidersDetailed = $($NoResourceProvidersDetailed))" -ForegroundColor Green $paramsUsed += "NoResourceProvidersDetailed: $($NoResourceProvidersDetailed) " @@ -20464,6 +20535,7 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $htNamingValidation.ManagementGroup = @{} #[System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} $htPrincipals = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} $htServicePrincipals = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} + $htDailySummary = @{} #subscriptions $startGetSubscriptions = get-date @@ -20803,6 +20875,7 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $htAzureEnvironmentRelatedUrls = $using:htAzureEnvironmentRelatedUrls $htBearerAccessToken = $using:htBearerAccessToken #Array&HTs + $htParameters = $using:htParameters $arrayAPICallTracking = $using:arrayAPICallTracking $allConsumptionData = $using:allConsumptionData $htSubscriptionsMgPath = $using:htSubscriptionsMgPath @@ -20983,6 +21056,7 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $htAzureEnvironmentRelatedUrls = $using:htAzureEnvironmentRelatedUrls $htBearerAccessToken = $using:htBearerAccessToken #Array&HTs + $htParameters = $using:htParameters $arrayAPICallTracking = $using:arrayAPICallTracking $htSubscriptionsMgPath = $using:htSubscriptionsMgPath $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI @@ -21205,7 +21279,6 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $requestPolicyDefinitionAPI = AzAPICall -uri $uri -method $method -currentTask $currentTask Write-Host " $($requestPolicyDefinitionAPI.Count) BuiltIn PolicyDefinitions returned by API" $builtinPolicyDefinitions = $requestPolicyDefinitionAPI.where( { $_.properties.policyType -eq "BuiltIn" } ) - Write-Host " $($builtinPolicyDefinitions.Count) BuiltIn PolicyDefinitions API filtered by BuiltIn" $builtInPolicyDefinitionsCount = $builtinPolicyDefinitions.Count foreach ($builtinPolicyDefinition in $builtinPolicyDefinitions) { @@ -21280,8 +21353,6 @@ if ($htParameters.HierarchyMapOnly -eq $false) { ($htCacheDefinitionsPolicy).(($builtinPolicyDefinition.Id).ToLower()).RoleDefinitionIds = "n/a" } } - Write-Host " ht $(($htCacheDefinitionsPolicy).Keys.Count) BuiltIn PolicyDefinitions" - #test $currentTask = "Caching built-in PolicySet definitions" Write-Host " $currentTask" @@ -21322,7 +21393,6 @@ if ($htParameters.HierarchyMapOnly -eq $false) { } ($htCacheDefinitionsPolicySet).(($builtinPolicySetDefinition.Id).ToLower()).Json = $builtinPolicySetDefinition } - Write-Host " ht $(($htCacheDefinitionsPolicySet).Keys.Count) BuiltIn PolicySetDefinitions" $currentTask = "Caching built-in Role definitions" Write-Host " $currentTask" @@ -21344,7 +21414,6 @@ if ($htParameters.HierarchyMapOnly -eq $false) { ($htCacheDefinitionsRole).($roleDefinition.name).Json = ($roleDefinition.properties) ($htCacheDefinitionsRole).($roleDefinition.name).LinkToAzAdvertizer = "$($roleDefinition.properties.roleName)" } - Write-Host " ht $(($htCacheDefinitionsRole).Keys.Count) BuiltIn RoleDefinitions" $endDefinitionsCaching = get-date Write-Host "Caching built-in definitions duration: $((NEW-TIMESPAN -Start $startDefinitionsCaching -End $endDefinitionsCaching).TotalSeconds) seconds" @@ -22037,10 +22106,6 @@ if ($htParameters.HierarchyMapOnly -eq $false) { } } } - #test - if ($cnty -gt 0) { - #Write-Host "$cnty / $cntx added as guest" - } } $script:htAADGroupsDetails.($aadGroupId).MembersAllCount = $aadGroupMembersAllCount @@ -22093,6 +22158,7 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $htAzureEnvironmentRelatedUrls = $using:htAzureEnvironmentRelatedUrls $htBearerAccessToken = $using:htBearerAccessToken #Array&HTs + $htParameters = $using:htParameters $htAADGroupsDetails = $using:htAADGroupsDetails $arrayGroupRoleAssignmentsOnServicePrincipals = $using:arrayGroupRoleAssignmentsOnServicePrincipals $arrayGroupRequestResourceNotFound = $using:arrayGroupRequestResourceNotFound @@ -22167,120 +22233,143 @@ if ($htParameters.HierarchyMapOnly -eq $false) { } #endregion dataprocessingAADGroups - #region NewAppSecret/Key and MI - Write-Host "Processing Service Principals (Applications and Managed Identities)" - $startSPAppMI = get-date - $htAppDetails = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} - $currentDateUTC = (Get-Date).ToUniversalTime() - $arrayApplicationRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + #region Application + Write-Host "Processing Service Principals - Applications" $servicePrincipalsOfTypeApplication = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq "Application" -and $htServicePrincipals.($_).appOwnerOrganizationId -eq $checkContext.Subscription.TenantId } ) - $servicePrincipalsOfTypeApplication | ForEach-Object -Parallel { - - #region UsingVARs - $currentDateUTC = $using:currentDateUTC - #fromOtherFunctions - $arrayAzureManagementEndPointUrls = $using:arrayAzureManagementEndPointUrls - $checkContext = $using:checkContext - $htAzureEnvironmentRelatedUrls = $using:htAzureEnvironmentRelatedUrls - $htBearerAccessToken = $using:htBearerAccessToken - #Array&HTs - $arrayApplicationRequestResourceNotFound = $using:arrayApplicationRequestResourceNotFound - $htAppDetails = $using:htAppDetails - $htServicePrincipals = $using:htServicePrincipals - #$arrayProgressedServicePrincipals = $using:arrayProgressedServicePrincipals - $arrayAPICallTracking = $using:arrayAPICallTracking - #$indicator = $using:indicator - #Functions - $function:AzAPICall = $using:funcAzAPICall - $function:createBearerToken = $using:funcCreateBearerToken - $function:GetJWTDetails = $using:funcGetJWTDetails - #endregion UsingVARs - - $sp = $htServicePrincipals.($_) - - $currentTask = "getApp $($sp.appId)" - $uri = "$(($htAzureEnvironmentRelatedUrls).($checkContext.Environment.Name).MSGraphUrl)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" + if ($UserType -eq "Guest") { + #testing if Guest has enough permissions + $app4Test = $htServicePrincipals.($servicePrincipalsOfTypeApplication[0]) + $currentTask = "getApp Test $($app4Test.appId)" + $uri = "$(($htAzureEnvironmentRelatedUrls).($checkContext.Environment.Name).MSGraphUrl)/v1.0/applications?`$filter=appId eq '$($app4Test.appId)'" $method = "GET" - $getApplication = AzAPICall -uri $uri -method $method -currentTask $currentTask -getApp $true + $testGetApplication = AzAPICall -uri $uri -method $method -currentTask $currentTask -getApp $true + if ($testGetApplication -eq "skipApplications") { + $skipApplications = $true + Write-Host " Guest account does not have enough permissions, skipping Applications (Secrets & Certificates)" + } + } + if (-not $skipApplications) { + $startSPApp = get-date + $htAppDetails = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} + $currentDateUTC = (Get-Date).ToUniversalTime() + $arrayApplicationRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $servicePrincipalsOfTypeApplication | ForEach-Object -Parallel { + + #region UsingVARs + $currentDateUTC = $using:currentDateUTC + #fromOtherFunctions + $arrayAzureManagementEndPointUrls = $using:arrayAzureManagementEndPointUrls + $checkContext = $using:checkContext + $htAzureEnvironmentRelatedUrls = $using:htAzureEnvironmentRelatedUrls + $htBearerAccessToken = $using:htBearerAccessToken + #Array&HTs + $htParameters = $using:htParameters + $arrayApplicationRequestResourceNotFound = $using:arrayApplicationRequestResourceNotFound + $htAppDetails = $using:htAppDetails + $htServicePrincipals = $using:htServicePrincipals + #$arrayProgressedServicePrincipals = $using:arrayProgressedServicePrincipals + $arrayAPICallTracking = $using:arrayAPICallTracking + #$indicator = $using:indicator + #Functions + $function:AzAPICall = $using:funcAzAPICall + $function:createBearerToken = $using:funcCreateBearerToken + $function:GetJWTDetails = $using:funcGetJWTDetails + #endregion UsingVARs + + $sp = $htServicePrincipals.($_) + + $currentTask = "getApp $($sp.appId)" + $uri = "$(($htAzureEnvironmentRelatedUrls).($checkContext.Environment.Name).MSGraphUrl)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" + $method = "GET" + $getApplication = AzAPICall -uri $uri -method $method -currentTask $currentTask -getApp $true - if ($getApplication -eq "Request_ResourceNotFound") { - $null = $arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ - appId = $sp.appId - }) - } - else { - if (($getApplication | Measure-Object).Count -eq 0) { - Write-Host "$($sp.appId) no data returned / seems non existent?" + if ($getApplication -eq "Request_ResourceNotFound") { + $null = $arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ + appId = $sp.appId + }) } else { - $script:htAppDetails.($sp.id) = @{} - $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType - $script:htAppDetails.($sp.id).spGraphDetails = $sp - $script:htAppDetails.($sp.id).appGraphDetails = $getApplication - - $appPasswordCredentialsCount = ($getApplication.passwordCredentials | Measure-Object).count - if ($appPasswordCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount - $appPasswordCredentialsExpiredCount = 0 - $appPasswordCredentialsGracePeriodExpiryCount = 0 - $appPasswordCredentialsExpiryOKCount = 0 - $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appPasswordCredential in $getApplication.passwordCredentials) { - $passwordExpiryTotalDays = (NEW-TIMESPAN -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays - if ($passwordExpiryTotalDays -lt 0) { - $appPasswordCredentialsExpiredCount++ - } - elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appPasswordCredentialsGracePeriodExpiryCount++ - } - else { - if ($passwordExpiryTotalDays -gt 730) { - $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ + if (($getApplication).Count -eq 0) { + Write-Host "$($sp.appId) no data returned / seems non existent?" + } + else { + $script:htAppDetails.($sp.id) = @{} + $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType + $script:htAppDetails.($sp.id).spGraphDetails = $sp + $script:htAppDetails.($sp.id).appGraphDetails = $getApplication + + $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count + if ($appPasswordCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount + $appPasswordCredentialsExpiredCount = 0 + $appPasswordCredentialsGracePeriodExpiryCount = 0 + $appPasswordCredentialsExpiryOKCount = 0 + $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appPasswordCredential in $getApplication.passwordCredentials) { + $passwordExpiryTotalDays = (NEW-TIMESPAN -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays + if ($passwordExpiryTotalDays -lt 0) { + $appPasswordCredentialsExpiredCount++ + } + elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appPasswordCredentialsGracePeriodExpiryCount++ } else { - $appPasswordCredentialsExpiryOKCount++ + if ($passwordExpiryTotalDays -gt 730) { + $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appPasswordCredentialsExpiryOKCount++ + } } } + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount + $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount } - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount - $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount - } - - $appKeyCredentialsCount = ($getApplication.keyCredentials | Measure-Object).count - if ($appKeyCredentialsCount -gt 0) { - $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount - $appKeyCredentialsExpiredCount = 0 - $appKeyCredentialsGracePeriodExpiryCount = 0 - $appKeyCredentialsExpiryOKCount = 0 - $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 - foreach ($appKeyCredential in $getApplication.keyCredentials) { - $keyCredentialExpiryTotalDays = (NEW-TIMESPAN -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays - if ($keyCredentialExpiryTotalDays -lt 0) { - $appKeyCredentialsExpiredCount++ - } - elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { - $appKeyCredentialsGracePeriodExpiryCount++ - } - else { - if ($keyCredentialExpiryTotalDays -gt 730) { - $appKeyCredentialsExpiryOKMoreThan2YearsCount++ + + $appKeyCredentialsCount = ($getApplication.keyCredentials | Measure-Object).count + if ($appKeyCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount + $appKeyCredentialsExpiredCount = 0 + $appKeyCredentialsGracePeriodExpiryCount = 0 + $appKeyCredentialsExpiryOKCount = 0 + $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appKeyCredential in $getApplication.keyCredentials) { + $keyCredentialExpiryTotalDays = (NEW-TIMESPAN -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays + if ($keyCredentialExpiryTotalDays -lt 0) { + $appKeyCredentialsExpiredCount++ + } + elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appKeyCredentialsGracePeriodExpiryCount++ } else { - $appKeyCredentialsExpiryOKCount++ + if ($keyCredentialExpiryTotalDays -gt 730) { + $appKeyCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appKeyCredentialsExpiryOKCount++ + } } } + $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount + $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount } - $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount - $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount - $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount } } - } - } -Throttlelimit ($ThrottleLimit * 2) + + } -Throttlelimit ($ThrottleLimit * 2) + + $endSPApp = get-date + Write-Host "Processing Service Principals - Applications duration: $((NEW-TIMESPAN -Start $startSPApp -End $endSPApp).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startSPApp -End $endSPApp).TotalSeconds) seconds)" + } + #endregion Application + #region ManagedIdentity + Write-Host "Processing Service Principals - Managed Identities" + $startSPMI = get-date $htManagedIdentityForPolicyAssignment = @{} $htPolicyAssignmentManagedIdentity = @{} $htManagedIdentityDisplayName = @{} @@ -22304,9 +22393,9 @@ if ($htParameters.HierarchyMapOnly -eq $false) { } } } - $endSPAppMI = get-date - Write-Host "Processing Service Principals (Applications and Managed Identities) duration: $((NEW-TIMESPAN -Start $startSPAppMI -End $endSPAppMI).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startSPAppMI -End $endSPAppMI).TotalSeconds) seconds)" - #endregion NewAppSecret/Key and MI + $endSPMI = get-date + Write-Host "Processing Service Principals - Managed Identities duration: $((NEW-TIMESPAN -Start $startSPMI -End $endSPMI).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startSPMI -End $endSPMI).TotalSeconds) seconds)" + #endregion ManagedIdentity #resourcesAll $resourcesAllGroupedBySubcriptionId = $resourcesAll | group-object -property subscriptionId @@ -22362,12 +22451,12 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $htAzureEnvironmentRelatedUrls = $using:htAzureEnvironmentRelatedUrls $htBearerAccessToken = $using:htBearerAccessToken #Array&HTs + $htParameters = $using:htParameters $ExludedResourceTypesDiagnosticsCapable = $using:ExludedResourceTypesDiagnosticsCapable $resourceTypesDiagnosticsArray = $using:resourceTypesDiagnosticsArray $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource $resourceTypesSummarizedArray = $using:resourceTypesSummarizedArray $arrayAPICallTracking = $using:arrayAPICallTracking - $htParameters = $using:htParameters #Functions $function:AzAPICallDiag = $using:funcAzAPICallDiag $function:createBearerToken = $using:funcCreateBearerToken @@ -22496,7 +22585,7 @@ if ($htParameters.HierarchyMapOnly -eq $false) { #region BuildHTML #testhelper -#$fileTimestamp = (get-date -format $FileTimeStampFormat) +$fileTimestamp = (get-date -format $FileTimeStampFormat) $startBuildHTML = get-date Write-Host "Building HTML" @@ -22796,30 +22885,84 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $totalResourceTypesCount = ($resourceTypesDiagnosticsArray).Count Write-Host " Total Management Groups: $totalMgCount (depth $mgDepth)" + $htDailySummary."ManagementGroups" = $totalMgCount Write-Host " Total Subscriptions: $totalSubIncludedAndExcludedCount ($totalSubCount included; $totalSubOutOfScopeCount out-of-scope)" + $htDailySummary."Subscriptions" = $totalSubCount + $htDailySummary."SubscriptionsOutOfScope" = $totalSubOutOfScopeCount Write-Host " Total Custom Policy definitions: $tenantCustomPoliciesCount" + $htDailySummary."PolicyDefinitionsCustom" = $tenantCustomPoliciesCount Write-Host " Total Custom PolicySet definitions: $tenantCustompolicySetsCount" + $htDailySummary."PolicySetDefinitionsCustom" = $tenantCustompolicySetsCount Write-Host " Total Policy assignments: $($totalPolicyAssignmentsCount)" + $htDailySummary."PolicyAssignments" = $totalPolicyAssignmentsCount Write-Host " Total Policy assignments ManagementGroups $($totalPolicyAssignmentsCountMg)" + $htDailySummary."PolicyAssignments_ManagementGroups" = $totalPolicyAssignmentsCountMg Write-Host " Total Policy assignments Subscriptions $($totalPolicyAssignmentsCountSub)" + $htDailySummary."PolicyAssignments_Subscriptions" = $totalPolicyAssignmentsCountSub Write-Host " Total Policy assignments ResourceGroups: $($totalPolicyAssignmentsCountRg)" + $htDailySummary."PolicyAssignments_ResourceGroups" = $totalPolicyAssignmentsCountRg Write-Host " Total Custom Role definitions: $totalRoleDefinitionsCustomCount" + $htDailySummary."RoleDefinitionsCustom" = $totalRoleDefinitionsCustomCount Write-Host " Total Role assignments: $totalRoleAssignmentsCount" + $htDailySummary."TotalRoleAssignments" = $totalRoleAssignmentsCount Write-Host " Total Role assignments (Tenant): $totalRoleAssignmentsCountTen" + $htDailySummary."TotalRoleAssignments_Tenant" = $totalRoleAssignmentsCountTen Write-Host " Total Role assignments (ManagementGroups): $totalRoleAssignmentsCountMG" + $htDailySummary."TotalRoleAssignments_ManagementGroups" = $totalRoleAssignmentsCountMG Write-Host " Total Role assignments (Subscriptions): $totalRoleAssignmentsCountSub" + $htDailySummary."TotalRoleAssignments_Subscriptions" = $totalRoleAssignmentsCountSub Write-Host " Total Role assignments (ResourceGroups and Resources): $totalRoleAssignmentsResourceGroupsAndResourcesCount" + $htDailySummary."TotalRoleAssignments_RgRes" = $totalRoleAssignmentsResourceGroupsAndResourcesCount Write-Host " Total Blueprint definitions: $totalBlueprintDefinitionsCount" + $htDailySummary."Blueprints" = $totalBlueprintDefinitionsCount Write-Host " Total Blueprint assignments: $totalBlueprintAssignmentsCount" + $htDailySummary."BlueprintAssignments" = $totalBlueprintAssignmentsCount Write-Host " Total Resources: $totalResourceCount" + $htDailySummary."Resources" = $totalResourceCount Write-Host " Total Resource Types: $totalResourceTypesCount" + $htDailySummary."ResourceTypes" = $totalResourceTypesCount + + $rbacUnique = $rbacAll | Sort-Object -Property RoleAssignmentId -Unique + $rbacUniqueObjectIds = $rbacUnique | Sort-Object -Property ObjectId -Unique + $rbacUniqueObjectIdsNonPIM = $rbacUnique.where( { $_.RoleAssignmentPIMRelated -eq $false } ) | Sort-Object -Property ObjectId -Unique + $rbacUniqueObjectIdsPIM = $rbacUnique.where( { $_.RoleAssignmentPIMRelated -eq $true } ) | Sort-Object -Property ObjectId -Unique + + if ($rbacUniqueObjectIds.Count -gt 0) { + $rbacUniqueObjectIdsGrouped = $rbacUniqueObjectIds | Group-Object -Property ObjectType + foreach ($principalType in $rbacUniqueObjectIdsGrouped) { + #Write-Host "$($principalType.Name): $($principalType.Count)" + $htDailySummary."TotalUniquePrincipalWithPermission_$($principalType.Name)" = $principalType.Count + } + $htDailySummary."TotalUniquePrincipalWithPermission_SP" = $rbacUniqueObjectIds.where( { $_.ObjectType -like "SP*" } ).count + $htDailySummary."TotalUniquePrincipalWithPermission_User" = $rbacUniqueObjectIds.where( { $_.ObjectType -like "User*" } ).count + } + + if ($rbacUniqueObjectIdsNonPIM.Count -gt 0) { + $rbacUniqueObjectIdsNonPIMGrouped = $rbacUniqueObjectIdsNonPIM | Group-Object -Property ObjectType + foreach ($principalType in $rbacUniqueObjectIdsNonPIMGrouped) { + #Write-Host "$($principalType.Name): $($principalType.Count)" + $htDailySummary."TotalUniquePrincipalWithPermissionStatic_$($principalType.Name)" = $principalType.Count + } + $htDailySummary."TotalUniquePrincipalWithPermissionStatic_SP" = $rbacUniqueObjectIdsNonPIM.where( { $_.ObjectType -like "SP*" } ).count + $htDailySummary."TotalUniquePrincipalWithPermissionStatic_User" = $rbacUniqueObjectIdsNonPIM.where( { $_.ObjectType -like "User*" } ).count + } + + if ($rbacUniqueObjectIdsPIM.Count -gt 0) { + $rbacUniqueObjectIdsPIMGrouped = $rbacUniqueObjectIdsPIM | Group-Object -Property ObjectType + foreach ($principalType in $rbacUniqueObjectIdsPIMGrouped) { + #Write-Host "$($principalType.Name): $($principalType.Count)" + $htDailySummary."TotalUniquePrincipalWithPermissionPIM_$($principalType.Name)" = $principalType.Count + } + $htDailySummary."TotalUniquePrincipalWithPermissionPIM_SP" = $rbacUniqueObjectIdsPIM.where( { $_.ObjectType -like "SP*" } ).count + $htDailySummary."TotalUniquePrincipalWithPermissionPIM_User" = $rbacUniqueObjectIdsPIM.where( { $_.ObjectType -like "User*" } ).count + } $endSummarizeDataCollectionResults = get-date Write-Host " Summary data collection duration: $((NEW-TIMESPAN -Start $startSummarizeDataCollectionResults -End $endSummarizeDataCollectionResults).TotalSeconds) seconds" #endregion summarizeDataCollectionResults } -$html += @" +$html = @" @@ -22952,9 +23095,70 @@ $html += @" document.body.removeChild(link); } + +"@ +if ($htParameters.HierarchyMapOnly -eq $false) { + if (-not $NoSingleSubscriptionOutput) { - + if ($AzureDevOpsWikiAsCode) { + $HTMLPath = "HTML-Subscriptions_$($ManagementGroupId)" + if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($HTMLPath)") { + Write-Host " Cleaning old state (Pipeline only)" + Remove-Item -Recurse -Force "$($outputPath)$($DirectorySeparatorChar)$($HTMLPath)" + } + } + else { + $HTMLPath = "HTML-Subscriptions_$($ManagementGroupId)_$($fileTimestamp)" + Write-Host " Creating new state ($($HTMLPath)) (local only))" + } + + $null = new-item -Name $HTMLPath -ItemType directory -path $outputPath + + $htmlSubscriptionOnlyStart = $html + $htmlSubscriptionOnlyStart += @" + +
+
+ +
+ +
+
+

ScopeInsights

+ +"@ + + $htmlSubscriptionOnlyEnd = @" +
+
+
+ + + + + + + + + + +"@ + } +} + + + +$html += @"
@@ -23218,19 +23422,19 @@ if ($htParameters.HierarchyMapOnly -eq $false) {
"@ - if (-not $NoScopeInsights) { + if ((-not $NoScopeInsights) -or (-not $NoSingleSubscriptionOutput)) { - $html += @" + if ((-not $NoScopeInsights)) { + $html += @"
-

ScopeInsights

+

ScopeInsights

"@ + $html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $html = $null + Write-Host " Building ScopeInsights" + } - - $html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force - $html = $null - Write-Host " Building ScopeInsights" $startHierarchyTable = get-date - $script:scopescnter = 0 tableMgHTML -mgChild $ManagementGroupIdCaseSensitived -mgChildOf $getMgParentId #[System.GC]::Collect() @@ -23238,11 +23442,12 @@ if ($htParameters.HierarchyMapOnly -eq $false) { $endHierarchyTable = get-date Write-Host " Building ScopeInsights duration: $((NEW-TIMESPAN -Start $startHierarchyTable -End $endHierarchyTable).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startHierarchyTable -End $endHierarchyTable).TotalSeconds) seconds)" - - $html += @" -
+ if ((-not $NoScopeInsights)) { + $html += @" +
"@ + } } } @@ -23425,6 +23630,17 @@ $endBuildMD = get-date Write-Host "Building Markdown total duration: $((NEW-TIMESPAN -Start $startBuildMD -End $endBuildMD).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startBuildMD -End $endBuildMD).TotalSeconds) seconds)" #endregion BuildMD +#region BuildDailySummaryCSV +$dailySummary4ExportToCSV = [System.Collections.ArrayList]@() +foreach ($entry in $htDailySummary.keys | sort-Object) { + $null = $dailySummary4ExportToCSV.Add([PSCustomObject]@{ + capability = $entry + count = $htDailySummary.($entry) + }) +} +$dailySummary4ExportToCSV | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_DailySummary.csv" -Delimiter "$csvDelimiter" -NoTypeInformation +#region BuildDailySummaryCSV + #region BuildConsumptionCSV if ($htParameters.HierarchyMapOnly -eq $false) { if ($htParameters.NoAzureConsumption -eq $false) { @@ -24157,7 +24373,7 @@ Write-Host "End AzGovViz $endTime" Write-Host "Checking for errors" if ($Error.Count -gt 0) { - Write-Host "Dumping $($Error.Count) Errors (handled by AzGovViz):" -ForegroundColor Yellow + Write-Host "Dumping $($Error.Count) Errors (handled by AzGovViz):" $Error | Out-host } else { @@ -24168,14 +24384,14 @@ else { if (-not $StatsOptOut) { if ($htParameters.AzureDevOpsWikiAsCode) { - if ($env:BUILD_REPOSITORY_ID){ + if ($env:BUILD_REPOSITORY_ID) { $hashTenantIdOrRepositoryId = [string]($env:BUILD_REPOSITORY_ID) } - else{ + else { $hashTenantIdOrRepositoryId = [string]($checkContext.Tenant.Id) } } - else{ + else { $hashTenantIdOrRepositoryId = [string]($checkContext.Tenant.Id) } @@ -24277,6 +24493,8 @@ if (-not $StatsOptOut) { "statsParametersNoASCSecureScore": "$($htParameters.NoASCSecureScore)", "statsParametersNoAzureConsumption": "$($htParameters.NoAzureConsumption)", "statsParametersNoJsonExport": "$($htParameters.NoJsonExport)", + "statsParametersNoScopeInsights": "$($htParameters.NoScopeInsights)", + "statsParametersNoSingleSubscriptionOutput": "$($NoSingleSubscriptionOutput)", "statsParametersNoPolicyComplianceStates": "$($htParameters.NoPolicyComplianceStates)", "statsParametersNoResourceProvidersDetailed": "$($htParameters.NoResourceProvidersDetailed)", "statsParametersNoResources": "$($htParameters.NoResources)", @@ -24337,4 +24555,11 @@ else { if ($DoTranscript) { Stop-Transcript +} + +Write-Host "" +Write-Host "--------------------" +Write-Host "Completed successful" -ForegroundColor Green +if ($Error.Count -gt 0) { + Write-Host "Don´t bother about dumped errors" } \ No newline at end of file diff --git a/setup.md b/setup.md index b60c0222..5bffdaf1 100644 --- a/setup.md +++ b/setup.md @@ -1,6 +1,6 @@ # AzGovViz - Azure Governance Visualizer - Setup -This guide will help you to setup and run AzGovViz. +This guide will help you to setup and run AzGovViz * Abbreviations: * Azure Active Directory - AAD * Azure DevOps - AzDO @@ -8,18 +8,13 @@ This guide will help you to setup and run AzGovViz. ## Table of contents * [__AzGovViz from Console__](#azgovviz-from-console) * Grant permissions in Azure + * Execution options + * Option 1 - Execute as a Tenant Member User + * Option 2 - Execute as a Tenant Guest User + * Option 3 - Execute as Service Principal * Clone the AzGovViz repository - * Option 1 - Execute as a Tenant Member User - * Run AzGovViz - * Option 2 - Execute as a Tenant Guest User - * Grant permissions in AAD - * Run AzGovViz - * Option 3 - Execute as Service Principal - * Grant permissions in AAD - * Option 1 - API permissions - * Option 2 - AAD Role - * Run AzGovViz - + * Run AzGovViz + * [__AzGovViz in Azure DevOps (AzDO)__](#azgovviz-in-azure-devops) * Create AzDO Project * Import AzGovViz Github repository @@ -28,8 +23,6 @@ This guide will help you to setup and run AzGovViz. * Option 2 - Create Service Connection´s Service Principal in the Azure Portal * Grant permissions in Azure * Grant permissions in AAD - * Option 1 - API permissions - * Option 2 - AAD Role * Grant permissions on AzGovViz AzDO repository * Edit AzDO YAML file * Create AzDO Pipeline @@ -41,9 +34,9 @@ This guide will help you to setup and run AzGovViz. ## Grant permissions in Azure * Requirements - * To assign roles, you must have Microsoft.Authorization/roleAssignments/write permissions, such as RBAC Role 'User Access Administrator' or 'Owner' on the target Management Group scope + * To assign roles, you must have '__Microsoft.Authorization/roleAssignments/write__' permissions on the target Management Group scope (such as the built-in RBAC Role '__User Access Administrator__' or '__Owner__') -Create a 'Reader' RBAC Role assignment on the target Management Group scope for the identity that shall run AzGovViz +Create a '__Reader__' RBAC Role assignment on the target Management Group scope for the identity that shall run AzGovViz * PowerShell ```powershell @@ -60,63 +53,71 @@ New-AzRoleAssignment ` * Azure Portal [Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) -## Clone the AzGovViz repository +## Execution options -* Requirements - * To clone the AzGovViz Github repository you need to have GIT installed - * Install Git: https://git-scm.com/download/win - -* PowerShell -```powershell -Set-Location "c:\Git" -git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" -``` +### Option 1 - Execute as a Tenant Member User -## Option 1 - Execute as a Tenant Member User +Proceed with step [__Clone the AzGovViz repository__](#clone-the-azgovviz-repository) -Proceed with step [__Run AzGovViz from Console__](#run-azgovviz-from-console) +### Option 2 - Execute as a Tenant Guest User -## Option 2 - Execute as a Tenant Guest User +If the tenant is hardened (AAD External Identities / Guest user access = most restrictive) then Guest User must be assigned the AAD Role '__Directory readers__' -A Tenant Guest User by default has less permissions in AAD than a Tenant Member User ([Compare member and guest default permissions](https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/fundamentals/users-default-permissions.md#compare-member-and-guest-default-permissions)), therefore we need to grant additional permissions in AAD by assigning AAD Role 'Directory Reader' for the Guest User. +💡 [Compare member and guest default permissions](https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/fundamentals/users-default-permissions.md#compare-member-and-guest-default-permissions) +💡 [Restrict Guest permissions](https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions) -### Option 2 - Execute as a Tenant Guest User - Grant permissions in AAD +#### Assign AAD Role - Directory readers * Requirements - * To assign roles, you must have 'Privileged Role Administrator' or 'Global Administrator' role assigned [Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal) + * To assign roles, you must have '__Privileged Role Administrator__' or '__Global Administrator__' role assigned [Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal) -Assign the AAD Role 'Directory Reader' for the Guest User that shall run AzGovViz (work with the Guest User´s display name) +Assign the AAD Role '__Directory Reader__' for the Guest User that shall run AzGovViz (work with the Guest User´s display name) * Azure Portal * [Assign a role](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal#assign-a-role) -Proceed with step [__Run AzGovViz from Console__](#run-azgovviz-from-console) +Proceed with step [__Clone the AzGovViz repository__](#clone-the-azgovviz-repository) -## Option 3 - Execute as Service Principal +### Option 3 - Execute as Service Principal -A Service Principal by default has no read permissions on Identities, Groups and other Service Principals (Applications, Managed Identities), therefore we need to grant additional permissions in AAD. +A Service Principal by default has no read permissions on Users, Groups and Service Principals, therefore we need to grant additional permissions in AAD -### Option 3 - Execute as Service Principal - Grant permissions in AAD +#### Grant API permissions -There are two options to grant the Service Principal the required permissions -* Options - * __Option 1__ API permissions (recommended) - * __Option 2__ AAD Role +* Requirements + * To grant API permissions and grant admin consent for the directory, you must have '__Privileged Role Administrator__' or '__Global Administrator__' role assigned [Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal) -#### Option 3 - Execute as Service Principal - Grant permissions in AAD - Option 1 - API permissions +Grant API permissions for the Service Principal´s Application +* Navigate to 'Azure Active Directory' +* Click on '__App registrations__' +* Search for the Application that we created earlier and click on it +* Under '__Manage__' click on '__API permissions__' + * Click on '__Add a permissions__' + * Click on '__Microsoft Graph__' + * Click on '__Application permissions__' + * Select the following set of permissions and click '__Add permissions__' + * __Application / Application.Read.All__ + * __Group / Group.Read.All__ + * __User / User.Read.All__ + * Click on 'Add a permissions' + * Back in the main '__API permissions__' menu you will find the 4 permissions with status 'Not granted for...'. Click on '__Grant admin consent for _TenantName___' and confirm by click on '__Yes__' + * Now you will find the 4 permissions with status '__Granted for _TenantName___' -* Requirements - * To grant API permissions and grant admin consent for the directory, you must have 'Privileged Role Administrator' or 'Global Administrator' role assigned [Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal) +Permissions in Azure Active Directory for App registration: +![alt text](img/aadpermissionsportal.jpg "Permissions in Azure Active Directory") -Proceed with step [__Run AzGovViz from Console__](#run-azgovviz-from-console) +Proceed with step [__Clone the AzGovViz repository__](#clone-the-azgovviz-repository) -#### Option 3 - Execute as Service Principal - Grant permissions in AAD - Option 2 - AAD Role +## Clone the AzGovViz repository * Requirements - * To assign roles, you must have 'Privileged Role Administrator' or 'Global Administrator' role assigned [Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal) + * To clone the AzGovViz Github repository you need to have GIT installed + * Install Git: https://git-scm.com/download/win -Assign the AAD Role 'Directory Reader' for the Service Principal that shall run AzGovViz (work with the Service Principal´s display name) -* Azure Portal - * [Assign a role](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal#assign-a-role) +* PowerShell +```powershell +Set-Location "c:\Git" +git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" +``` Proceed with step [__Run AzGovViz from Console__](#run-azgovviz-from-console) @@ -131,7 +132,7 @@ Proceed with step [__Run AzGovViz from Console__](#run-azgovviz-from-console) * [Installing PowerShell on Linux](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux) * Requires PowerShell Az Modules * Az.Accounts - * Az.Resources + * ~~Az.Resources~~ * [Install the Azure Az PowerShell module](https://docs.microsoft.com/en-us/powershell/azure/install-az-ps) ### Connecting to Azure as User (Member or Guest) @@ -143,14 +144,14 @@ Connect-AzAccount -TenantId -UseDeviceAuthentication ### Connecting to Azure using Service Principal -Have the 'Application (client) ID' of the App registration OR 'Application ID' of the Service Principal (Enterprise Application) and the secret of the App registration at hand. +Have the '__Application (client) ID__' of the App registration OR '__Application ID__' of the Service Principal (Enterprise Application) and the secret of the App registration at hand * PowerShell ```powershell $pscredential = Get-Credential Connect-AzAccount -ServicePrincipal -TenantId -Credential $pscredential ``` -User: Enter 'Application (client) ID' of the App registration OR 'Application ID' of the Service Principal (Enterprise Application) +User: Enter '__Application (client) ID__' of the App registration OR '__Application ID__' of the Service Principal (Enterprise Application) Password for user \: Enter App registration´s secret ### Run AzGovViz @@ -185,60 +186,61 @@ Note: the AzGovViz GitHub repository is public - no authorization required ## Create AzDO Service Connection -For the pipeline to authenticate and connect to Azure we need to create an AzDO Service Connection which basically is a Service Principal (Application). -There are two options to create the Service Connection. +For the pipeline to authenticate and connect to Azure we need to create an AzDO Service Connection which basically is a Service Principal (Application) +There are two options to create the Service Connection: * Options - * __Option 1__ Create Service Connection´s Service Principal in the Azure Portal (recommended) + * __Option 1__ Create Service Connection´s Service Principal in the Azure Portal * __Option 2__ Create Service Connection in AzDO ### Create AzDO Service Connection - Option 1 - Create Service Connection´s Service Principal in the Azure Portal -Azure Portal +#### Azure Portal * Navigate to 'Azure Active Directory' -* Click on 'App registrations' -* Click on 'New registration' +* Click on '__App registrations__' +* Click on '__New registration__' * Name your application (e.g. 'AzGovViz_SC') -* Click 'Register' -* Your App registration has been created, in the 'Overview' copy the 'Application (client) ID' as we will need it later to setup the Service Connection in AzDO -* Under 'Manage' click on 'Certificates & Secrets' -* Click on 'New client secret' -* Provide a good description and choose the expiry time based on your need and click 'Add' +* Click '__Register__' +* Your App registration has been created, in the '__Overview__' copy the '__Application (client) ID__' as we will need it later to setup the Service Connection in AzDO +* Under '__Manage__' click on '__Certificates & Secrets__' +* Click on '__New client secret__' +* Provide a good description and choose the expiry time based on your need and click '__Add__' * A new client secret has been created, copy the secret´s value as we will need it later to setup the Service Connection in AzDO -Azure DevOps (AzDO) -* Click on 'Project settings' (located on the bottom left) -* Under 'Pipelines' click on 'Service Connections' -* Click on 'New service connection' and select the connection/service type 'Azure Resource Manager' and click Next -* For the authentication method select 'Service principal (manual)' and click Next -* For the 'Scope level' select 'Management Group' - * In the field 'Management Group Id' enter the target Management Group Id - * In the field 'Management Group Name' enter the target Management Group Name -* Under 'Authentication' in the field 'Service Principal Id' enter the 'Application (client) ID' that you copied earlier -* For the 'Credential' select 'Service principal key', in the field 'Service principal key' enter the secret that you copied earlier -* For 'Tenant ID' enter your Tenant Id -* Click on 'Verify' -* Under 'Details' provide your Service Connection with a name and copy the name as we will need that later when editing the Pipeline YAML file -* For 'Security' leave the 'Grant access permissions to all pipelines' option checked -* Click on 'Verify and save' +#### Azure DevOps +* Click on '__Project settings__' (located on the bottom left) +* Under '__Pipelines__' click on '__Service Connections__' +* Click on '__New service connection__' and select the connection/service type '__Azure Resource Manager__' and click '__Next__' +* For the authentication method select '__Service principal (manual)__' and click '__Next__' +* For the '__Scope level__' select '__Management Group__' + * In the field '__Management Group Id__' enter the target Management Group Id + * In the field '__Management Group Name__' enter the target Management Group Name +* Under '__Authentication__' in the field '__Service Principal Id__' enter the '__Application (client) ID__' that you copied away earlier +* For the '__Credential__' select '__Service principal key__', in the field '__Service principal key__' enter the secret that you copied away earlier +* For '__Tenant ID__' enter your Tenant Id +* Click on '__Verify__' +* Under '__Details__' provide your Service Connection with a name and copy away the name as we will need that later when editing the Pipeline YAML file +* For '__Security__' leave the 'Grant access permissions to all pipelines' option checked (optional) +* Click on '__Verify and save__' ### Create AzDO Service Connection - Option 2 - Create Service Connection in AzDO -* Click on 'Project settings' (located on the bottom left) -* Under 'Pipelines' click on 'Service connections' -* Click on 'New service connection' and select the connection/service type 'Azure Resource Manager' and click Next -* For the authentication method select 'Service principal (automatic)' and click Next -* For the 'Scope level' select 'Management Group', in the Management Group dropdown select the target Management Group (here the Management Group´s display names will be shown), in the 'Details' section apply a Service Connection name and optional give it a description and click Save +* Click on '__Project settings__' (located on the bottom left) +* Under '__Pipelines__' click on '__Service connections__' +* Click on '__New service connection__' and select the connection/service type '__Azure Resource Manager__' and click '__Next__' +* For the authentication method select '__Service principal (automatic)__' and click '__Next__' +* For the '__Scope level__' select '__Management Group__', in the Management Group dropdown select the target Management Group (here the Management Group´s display names will be shown), in the '__Details__' section apply a Service Connection name and optional give it a description and click '__Save__' * A new window will open, authenticate with your administrative account -* Now the Service Connection has been created -* __Important!__ In Azure on the target Management Group scope a 'Owner' RBAC Role assignment for the Service Connection´s Service Principal has been created automatically (we do however only require a 'Reader' RBAC Role assignment! we will take corrective action in the next steps) +* Now the Service Connection has been created + +__Important!__ In Azure on the target Management Group scope an '__Owner__' RBAC Role assignment for the Service Connection´s Service Principal has been created automatically (we do however only require a '__Reader__' RBAC Role assignment! we will take corrective action in the next steps) ## Grant permissions in Azure * Requirements - * To assign roles, you must have Microsoft.Authorization/roleAssignments/write permissions, such as RBAC Role 'User Access Administrator' or 'Owner' on the target Management Group scope + * To assign roles, you must have '__Microsoft.Authorization/roleAssignments/write__' permissions on the target Management Group scope (such as the built-in RBAC Role '__User Access Administrator__' or '__Owner__') -Create a 'Reader' RBAC Role assignment on the target Management Group scope for the AzDO Service Connection´s Service Principal +Create a '__Reader__' RBAC Role assignment on the target Management Group scope for the AzDO Service Connection´s Service Principal * PowerShell ```powershell @@ -255,93 +257,78 @@ New-AzRoleAssignment ` * Azure Portal [Assign Azure roles using the Azure portal](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) -__Important!__ If you have created the AzDO Service Connection in AzDO (Option 2) then you SHOULD remove the automatically created 'Owner' RBAC Role assignment for the AzDO Service Connection´s Service Principal from the target Management Group +__Important!__ If you have created the AzDO Service Connection in AzDO (Option 2) then you SHOULD remove the automatically created '__Owner__' RBAC Role assignment for the AzDO Service Connection´s Service Principal from the target Management Group ## Grant permissions in AAD -There are two options to grant the AzDO Service Connection´s Service Principal the required permissions -* Options - * __Option 1__ API permissions (recommended) - * __Option 2__ AAD Role - -### Grant permissions in AAD - Option 1 - API permissions +### API permissions * Requirements - * To grant API permissions and grant admin consent for the directory, you must have 'Privileged Role Administrator' or 'Global Administrator' role assigned ([Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal)) + * To grant API permissions and grant admin consent for the directory, you must have '__Privileged Role Administrator__' or '__Global Administrator__' role assigned ([Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal)) -Grant API permissions for the Application that we created earlier +Grant API permissions for the Service Principal´s Application that we created earlier * Navigate to 'Azure Active Directory' -* Click on 'App registrations' +* Click on '__App registrations__' * Search for the Application that we created earlier and click on it -* Under 'Manage' click on 'API permissions' - * Click on 'Add a permissions' +* Under '__Manage__' click on '__API permissions__' + * Click on '__Add a permissions__' * Click on '__Microsoft Graph__' - * Click on 'Application permissions' - * Select the following set of permissions and click 'Add permissions' - * Application / Application.Read.All - * Group / Group.Read.All - * User / User.Read.All + * Click on '__Application permissions__' + * Select the following set of permissions and click '__Add permissions__' + * __Application / Application.Read.All__ + * __Group / Group.Read.All__ + * __User / User.Read.All__ * Click on 'Add a permissions' - * Back in the main 'API permissions' menu you will find the 4 permissions with status 'Not granted for...'. Click on 'Grant admin consent for _TenantName_' and confirm by click on 'Yes' - * Now you will find the 4 permissions with status 'Granted for _TenantName_' + * Back in the main '__API permissions__' menu you will find the 4 permissions with status 'Not granted for...'. Click on '__Grant admin consent for _TenantName___' and confirm by click on '__Yes__' + * Now you will find the 4 permissions with status '__Granted for _TenantName___' Permissions in Azure Active Directory for App registration: ![alt text](img/aadpermissionsportal.jpg "Permissions in Azure Active Directory") - -### Grant permissions in AAD - Option 2 - AAD Role - -* Requirements - * To assign roles, you must have 'Privileged Role Administrator' or 'Global Administrator' role assigned ([Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal)) - -Assign the AAD Role 'Directory Reader' for the AzDO Service Connection´s Service Principal (work with the Service Principal´s display name) -* Azure Portal - * [Assign AAD role](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal#assign-a-role) - ## Grant permissions on AzGovViz AzDO repository When the AzDO pipeline executes the AzGovViz script the outputs should be pushed back to the AzGovViz AzDO repository, in order to do this we need to grant the AzDO Project´s Build Service account with 'Contribute' permissions on the repository * Grant permissions on the AzGovViz AzDO repository - * In AzDO click on 'Project settings' (located on the bottom left), under 'Repos' open the 'Repositories' page - * Click on the AzGovViz AzDO Repository and select the tab 'Security' + * In AzDO click on '__Project settings__' (located on the bottom left), under '__Repos__' open the '__Repositories__' page + * Click on the AzGovViz AzDO Repository and select the tab '__Security__' * On the right side search for the Build Service account - __%Project name% Build Service (%Organization name%)__ and grant it with 'Contribute' permissions by selecting 'Allow' (no save button available) + __%Project name% Build Service (%Organization name%)__ and grant it with '__Contribute__' permissions by selecting '__Allow__' (no save button available) ## Edit AzDO YAML file -* Click on 'Repos' +* Click on '__Repos__' * Navigate to the AzGovViz Repository -* In the folder 'pipeline' click on 'AzGovViz.yml' and click 'Edit' +* In the folder '__pipeline__' click on '__AzGovViz.yml__' and click '__Edit__' * Under the variables section * Enter the Service Connection name that you copied earlier (ServiceConnection) * Enter the Management Group Id (ManagementGroupId) -* Click 'Commit' +* Click '__Commit__' ## Create AzDO Pipeline -* Click on 'Pipelines' -* Click on 'New pipeline' -* Select 'Azure Repos Git' +* Click on '__Pipelines__' +* Click on '__New pipeline__' +* Select '__Azure Repos Git__' * Select the AzGovViz repository -* Click on 'Existing Azure Pipelines YAML file' -* Under 'Path' select '/pipeline/AzGovViz.yml' (the YAML file we edited earlier) -* Click ' Continue' +* Click on '__Existing Azure Pipelines YAML file__' +* Under '__Path__' select '__/pipeline/AzGovViz.yml__' (the YAML file we edited earlier) +* Click ' __Continue__' ## Run the AzDO Pipeline -* Click on 'Pipelines' +* Click on '__Pipelines__' * Select the AzGovViz pipeline -* Click 'Run pipeline' +* Click '__Run pipeline__' ## Create AzDO Wiki (WikiAsCode) Once the pipeline has executed successfully we can setup our Wiki (WikiAsCode) -* Click on 'Overview' -* Click on 'Wiki' -* Click on 'Publish code as wiki' +* Click on '__Overview__' +* Click on '__Wiki__' +* Click on '__Publish code as wiki__' * Select the AzGovViz repository -* Select the folder 'wiki' and click 'OK' +* Select the folder '__wiki__' and click '__OK__' * Enter a name for the Wiki -* Click 'Publish' +* Click '__Publish__' \ No newline at end of file From 214ba24b08f8def25fa4db88b613de164214893b Mon Sep 17 00:00:00 2001 From: quoteee <45695032+JulianHayward@users.noreply.github.com> Date: Tue, 2 Nov 2021 07:06:48 +0100 Subject: [PATCH 2/2] v6_major_20211101_1 --- pwsh/AzGovVizParallel.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 97f8f882..ca537149 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -24493,7 +24493,7 @@ if (-not $StatsOptOut) { "statsParametersNoASCSecureScore": "$($htParameters.NoASCSecureScore)", "statsParametersNoAzureConsumption": "$($htParameters.NoAzureConsumption)", "statsParametersNoJsonExport": "$($htParameters.NoJsonExport)", - "statsParametersNoScopeInsights": "$($htParameters.NoScopeInsights)", + "statsParametersNoScopeInsights": "$($NoScopeInsights)", "statsParametersNoSingleSubscriptionOutput": "$($NoSingleSubscriptionOutput)", "statsParametersNoPolicyComplianceStates": "$($htParameters.NoPolicyComplianceStates)", "statsParametersNoResourceProvidersDetailed": "$($htParameters.NoResourceProvidersDetailed)",