diff --git a/.azuredevops/pipelines/AzGovViz.variables.yml b/.azuredevops/pipelines/AzGovViz.variables.yml index 82103ca0..66798ddc 100644 --- a/.azuredevops/pipelines/AzGovViz.variables.yml +++ b/.azuredevops/pipelines/AzGovViz.variables.yml @@ -1,4 +1,4 @@ -# AzGovViz v6_major_20220912_1 +# AzGovViz v6_major_20220927_1 # First things first: # 1. Replace with the name of your service connection # 2. Replace with the your ManagementGroupId @@ -28,6 +28,26 @@ parameters: default: - undefined + # Subscription Tag names for Storage Account Access Analysis + - name: StorageAccountAccessAnalysisSubscriptionTagsParameters + type: object + # example: + # default: + # - Responsible + # - + default: + - undefined + + # Storage Account Tag names for Storage Account Access Analysis + - name: StorageAccountAccessAnalysisStorageAccountTagsParameters + type: object + # example: + # default: + # - SAOwner + # - + default: + - undefined + variables: ### Required Variables @@ -279,8 +299,8 @@ variables: # Switch | example: value: true value: -# Do not execute Azure Landing Zones Evergreen - - name: NoALZEvergreen +# Do not execute Azure Landing Zones (ALZ) Policy Version Checker + - name: noALZPolicyVersionChecker # Switch | example: value: true value: @@ -295,3 +315,9 @@ variables: - name: SubscriptionQuotaIdWhitelist value: ${{ join(',',parameters.SubscriptionQuotaIdWhitelistParameters) }} + + - name: StorageAccountAccessAnalysisSubscriptionTags + value: ${{ join(',',parameters.StorageAccountAccessAnalysisSubscriptionTagsParameters) }} + + - name: StorageAccountAccessAnalysisStorageAccountTags + value: ${{ join(',',parameters.StorageAccountAccessAnalysisStorageAccountTagsParameters) }} diff --git a/README.md b/README.md index 55bf1b37..76fbc1d9 100644 --- a/README.md +++ b/README.md @@ -59,26 +59,16 @@ Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/arch ## Release history -__Changes__ (2022-Sep-17 / Major) - -* Fix Azure DevOps Pipeline correct addressing of NoDefinitionInsights variable in YAML -* Fix issue #132 -* Add __[Contribution Guide](contributionGuide.md)__ - -__Changes__ (2022-Sep-12 / Major) - -* New feature 'ALZ EverGreen' - Azure Landing Zones EverGreen for Policy and Set definitions. AzGovViz will clone the ALZ GitHub repository and collect the ALZ policy and set definitions history. The ALZ data will be compared with the data from your tenant so that you can get lifecycle management recommendations for ALZ policy and set definitions that already exist in your tenant plus a list of ALZ policy and set definitions that do not exist in your tenant. The ALZ EverGreen results will be displayed in the __TenantSummary__ and a CSV export `*_ALZEverGreen.csv` will be provided. Thanks! ALZ Team - * New parameter `-NoALZEverGreen` - Do not execute the ALZ EverGreen feature -* Update: Per default __DefinitionInsights__ will be written to a separate HTML file. This will improve the html file handling (browser memory usage /response time / user experience). - * Note: Please update your Azure DevOps and GitHub YAML files with the latest versions if you are using the webApp publishing feature - * New parameter `-NoDefinitionInsightsDedicatedHTML` (__DefinitionInsights__ will NOT be written to a separate HTML file `*_DefinitionInsights.html`) -* Add Resource fluctuation detailed (`*_ResourceFluctuationDetailed.csv`) CSV output (add/remove, scope details, resource details) -* Fix consumption reporting for large tenants with more than 3k subscriptions (_Management Group abc has too many subscriptions , exceeding CCM API Current Limit 3000_) -* Fix CSV export `*_PolicySetDefinitions.csv` - Builtin Policy definitions contained in PolicySet definitions will only show the GUID instead of the full ID as for large PolicySet definitions the field size limit in Excel may be exceeded (column: PoliciesUsed4CSV) -* BuiltIn definitions collection - add 'Static' Policy definitions (part of __DefinitionInsights__ and `*_PolicyDefinitions.csv`) -* Fix __HierarchyMap__ image quality (now .png (aka 'peng')). Thanks! Brooks Vaughn -* Use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.23 -* Optimizations +__Changes__ (2022-Sep-27 / Major) + +* New feature 'Storage Account Access Analysis' - provides insights on Storage Accounts focusing on anonymous access (containers/blobs and static website feature). Data is provided in the HTML __TenantSummary__ (Subscriptions, Resources & Defender) and as CSV export + * New parameter `-NoStorageAccountAccessAnalysis` - do not execute the feature + * New parameter `-StorageAccountAccessAnalysisSubscriptionTags` - define the Subscription tags that should be added to the CSV output + * New parameter `-StorageAccountAccessAnalysisStorageAccountTags` - define the Storage Account (resource) tags that should be added to the CSV output + * Updated `.azuredevops/pipelines/AzGovViz.variables.yml` accordingly +* Rename 'ALZ EverGreen' feature to 'Azure Landing Zones (ALZ) Policy Version Checker' + * Replaced parameter `-NoALZEverGreen` `-NoALZPolicyVersionChecker` +* Use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.24 Passed tests: Powershell Core 7.2.6 on Windows Passed tests: Powershell Core 7.2.6 Azure DevOps hosted agent ubuntu-20.04 @@ -493,7 +483,7 @@ AzAPICall resources: * `-PIMEligibilityIgnoreScope` - By default will only report for PIM Elibility for the scope (`ManagementGroupId`) that was provided. If you use the new switch parameter then PIM Eligibility for all onboarded scopes (Management Groups and Subscriptions) will be reported * `-NoPIMEligibilityIntegrationRoleAssignmentsAll` - Prevent integration of PIM eligible assignments with RoleAssignmentsAll (HTML, CSV) * ~~`-DefinitionInsightsDedicatedHTML`~~ `-NoDefinitionInsightsDedicatedHTML` - __DefinitionInsights__ will be written to a separate HTML file `*_DefinitionInsights.html`. If you want to keep __DefinitionInsights__ in the main html file then use this parameter - * `-NoALZEvergreen` - Do not execute the ALZ EverGreen feature + * ~~`-NoALZEvergreen`~~ `-NoALZPolicyVersionChecker` - Do not execute the ~~'ALZ EverGreen'~~ 'Azure Landing Zones (ALZ) Policy Version Checker' feature ### API reference diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index f7f1a804..9ffd2f18 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -152,15 +152,28 @@ Prevent integration of PIM eligible assignments with RoleAssignmentsAll (HTML, CSV) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoPIMEligibilityIntegrationRoleAssignmentsAll -.PARAMETER NoALZEvergreen - ALZ EverGreen - Azure Landing Zones EverGreen for Policy and Set definitions. AzGovViz will clone the ALZ GitHub repository and collect the ALZ policy and set definitions history. The ALZ data will be compared with the data from your tenant so that you can get lifecycle management recommendations for ALZ policy and set definitions that already exist in your tenant plus a list of ALZ policy and set definitions that do not exist in your tenant. The ALZ EverGreen results will be displayed in the TenantSummary and a CSV export `*_ALZEverGreen.csv` will be provided. - If you do not want to execute the ALZ EverGreen feature then use this parameter - PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZEvergreen +.PARAMETER NoALZPolicyVersionChecker + 'Azure Landing Zones (ALZ) Policy Version Checker' for Policy and Set definitions. AzGovViz will clone the ALZ GitHub repository and collect the ALZ policy and set definitions history. The ALZ data will be compared with the data from your tenant so that you can get lifecycle management recommendations for ALZ policy and set definitions that already exist in your tenant plus a list of ALZ policy and set definitions that do not exist in your tenant. The 'Azure Landing Zones (ALZ) Policy Version Checker' results will be displayed in the TenantSummary and a CSV export `*_ALZPolicyVersionChecker.csv` will be provided. + If you do not want to execute the 'Azure Landing Zones (ALZ) Policy Version Checker' feature then use this parameter + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZPolicyVersionChecker .PARAMETER NoDefinitionInsightsDedicatedHTML DefinitionInsights will be written to a separate HTML file `*_DefinitionInsights.html`. If you want to keep DefinitionInsights in the main html file then use this parameter PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoDefinitionInsightsDedicatedHTML +.PARAMETER NoStorageAccountAccessAnalysis + Analysis on Storage Accounts, specially focused on anonymous access. + If you do not want to execute this feature then use this parameter + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoStorageAccountAccessAnalysis + +.PARAMETER StorageAccountAccessAnalysisSubscriptionTags + If the Storage Account Access Analysis feature is executed with this parameter you can define the subscription tags that should be added to the CSV output + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -StorageAccountAccessAnalysisSubscriptionTags @('Responsible', 'TeamEmail') + +.PARAMETER StorageAccountAccessAnalysisStorageAccountTags + If the Storage Account Access Analysis feature is executed with this parameter you can define the Storage Account (resource) tags that should be added to the CSV output + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -StorageAccountAccessAnalysisStorageAccountTags @('SAResponsible', 'DataOfficer') + .EXAMPLE Define the ManagementGroup ID PS C:\> .\AzGovVizParallel.ps1 -ManagementGroupId @@ -297,12 +310,17 @@ Define if PIM Eligible assignments should not be integrated with RoleAssignmentsAll outputs (HTML, CSV) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoPIMEligibilityIntegrationRoleAssignmentsAll - Define if the ALZ EverGreen feature should not be executed - PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZEvergreen + Define if the 'Azure Landing Zones (ALZ) Policy Version Checker' feature should not be executed + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZPolicyVersionChecker Define if DefinitionInsights should not be written to a seperate html file (*_DefinitionInsights.html) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoDefinitionInsightsDedicatedHTML + Define if Storage Account Access Analysis (focus on anonymous access) should be executed + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoStorageAccountAccessAnalysis + Additionally you can define Subscription and/or Storage Account Tag names that should be added to the CSV output per Storage Account + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId --StorageAccountAccessAnalysisSubscriptionTags @('Responsible', 'TeamEmail') -StorageAccountAccessAnalysisStorageAccountTags @('SAResponsible', 'DataOfficer') + .NOTES AUTHOR: Julian Hayward - Customer Engineer - Customer Success Unit | Azure Infrastucture/Automation/Devops/Governance | Microsoft @@ -319,10 +337,10 @@ Param $Product = 'AzGovViz', [string] - $AzAPICallVersion = '1.1.23', + $AzAPICallVersion = '1.1.24', [string] - $ProductVersion = 'v6_major_20220917_1', + $ProductVersion = 'v6_major_20220927_1', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -489,11 +507,20 @@ Param $NoPIMEligibilityIntegrationRoleAssignmentsAll, [switch] - $NoALZEvergreen, + $NoALZPolicyVersionChecker, [switch] $NoDefinitionInsightsDedicatedHTML, + [switch] + $NoStorageAccountAccessAnalysis, + + [array] + $StorageAccountAccessAnalysisSubscriptionTags = @('undefined'), + + [array] + $StorageAccountAccessAnalysisStorageAccountTags = @('undefined'), + #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits [int] $LimitRBACCustomRoleDefinitionsTenant = 5000, @@ -590,7 +617,8 @@ function addHtParameters { RBACAtScopeOnly = [bool]$RBACAtScopeOnly DoPSRule = [bool]$DoPSRule PSRuleFailedOnly = [bool]$PSRuleFailedOnly - NoALZEvergreen = [bool]$NoALZEvergreen + NoALZPolicyVersionChecker = [bool]$NoALZPolicyVersionChecker + NoStorageAccountAccessAnalysis = [bool]$NoStorageAccountAccessAnalysis } Write-Host 'htParameters:' $azAPICallConf['htParameters'] | format-table -AutoSize | Out-String @@ -3831,9 +3859,9 @@ function processAADGroups { Write-Host "Resolving AAD Groups duration: $((NEW-TIMESPAN -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving AAD Groups)" } -function processALZEverGreen { +function processALZPolicyVersionChecker { $start = get-date - Write-Host "Processing ALZ EverGreen base data" + Write-Host "Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data" $ALZRepositoryURI = 'https://github.com/Azure/Enterprise-Scale.git' $workingPath = Get-Location Write-Host " Working directory is '$($workingPath)'" @@ -3859,9 +3887,9 @@ function processALZEverGreen { if (-not (Test-Path -LiteralPath "$($ALZPath)/Enterprise-Scale" -PathType Container)) { $ALZCloneSuccess = $false Write-Host " Cloning '$($ALZRepositoryURI)' failed" - Write-Host " Setting switch parameter '-NoALZEvergreen' to true" - $script:NoALZEvergreen = $true - $script:azAPICallConf['htParameters'].NoALZEvergreen = $true + Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" + $script:NoALZPolicyVersionChecker = $true + $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true Write-Host " Switching back to working directory '$($workingPath)'" Set-Location $workingPath } @@ -3873,9 +3901,9 @@ function processALZEverGreen { catch { $_ Write-Host " Cloning '$($ALZRepositoryURI)' failed" - Write-Host " Setting switch parameter '-NoALZEvergreen' to true" - $script:NoALZEvergreen = $true - $script:azAPICallConf['htParameters'].NoALZEvergreen = $true + Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" + $script:NoALZPolicyVersionChecker = $true + $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true Write-Host " Switching back to working directory '$($workingPath)'" Set-Location $workingPath } @@ -4402,7 +4430,7 @@ function processALZEverGreen { } $end = Get-Date - Write-Host " Processing ALZ EverGreen base data duration: $((NEW-TIMESPAN -Start $start -End $end).TotalSeconds) seconds" + Write-Host " Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data duration: $((NEW-TIMESPAN -Start $start -End $end).TotalSeconds) seconds" } function processApplications { Write-Host 'Processing Service Principals - Applications' @@ -4823,6 +4851,7 @@ function processDataCollection { $scriptPath = $using:ScriptPath #Array&HTs $newTable = $using:newTable + $storageAccounts = $using:storageAccounts $resourcesAll = $using:resourcesAll $resourcesIdsAll = $using:resourcesIdsAll $resourceGroupsAll = $using:resourceGroupsAll @@ -4888,6 +4917,7 @@ function processDataCollection { $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub $function:dataCollectionResources = $using:funcDataCollectionResources + $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders $function:dataCollectionFeatures = $using:funcDataCollectionFeatures @@ -4963,6 +4993,14 @@ function processDataCollection { } DataCollectionDiagnosticsSub @baseParameters @dataCollectionDiagnosticsSubParameters + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + #resources + $dataCollectionStorageAccountsParameters = @{ + ChildMgMgPath = $childMgMgPath + ChildMgParentNameChainDelimited = $childMgParentNameChainDelimited + } + DataCollectionStorageAccounts @baseParameters @dataCollectionStorageAccountsParameters + } if ($azAPICallConf['htParameters'].NoResources -eq $false) { #resources @@ -10920,6 +10958,259 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { } } +function processStorageAccountAnalysis { + $start = get-date + Write-Host "Processing Storage Account Analysis" + $storageAccountscount = $storageAccounts.count + if ($storageAccountscount -gt 0) { + Write-Host " Executing Storage Account Analysis for $storageAccountscount Storage Accounts" + $script:arrayStorageAccountAnalysisResults = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + + $storageAccounts | ForEach-Object -Parallel { + $storageAccount = $_ + $azAPICallConf = $using:azAPICallConf + $arrayStorageAccountAnalysisResults = $using:arrayStorageAccountAnalysisResults + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htSubscriptionTags = $using:htSubscriptionTags + $CSVDelimiterOpposite = $using:CSVDelimiterOpposite + $StorageAccountAccessAnalysisSubscriptionTags = $using:StorageAccountAccessAnalysisSubscriptionTags + $StorageAccountAccessAnalysisStorageAccountTags = $using:StorageAccountAccessAnalysisStorageAccountTags + $listContainersSuccess = 'n/a' + $containersCount = 'n/a' + $arrayContainers = @() + $arrayContainersAnonymousContainer = @() + $arrayContainersAnonymousBlob = @() + $staticWebsitesState = 'n/a' + $webSiteResponds = 'n/a' + + $subscriptionId = ($storageAccount.id -split '/')[2] + $resourceGroupName = ($storageAccount.id -split '/')[4] + $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails + + Write-Host "Processing SA; Subscription: $($subDetails.displayName) ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)] - Storage Account: $($storageAccount.name)" + + if ($storageAccount.Properties.primaryEndpoints.blob) { + + $urlServiceProps = "$($storageAccount.Properties.primaryEndpoints.blob)?restype=service&comp=properties" + $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.name) get restype=service&comp=properties" + try { + $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) + } + catch { + Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.name)" + $saProperties | ConvertTo-Json -Depth 99 + } + + $staticWebsitesState = $false + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { + $staticWebsitesState = $true + } + } + + $urlCompList = "$($storageAccount.Properties.primaryEndpoints.blob)?comp=list" + $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.name) get comp=list" + if ($listContainers -eq 'AuthorizationFailure') { + $listContainersSuccess = $false + } + else { + $listContainersSuccess = $true + } + + if ($listContainersSuccess) { + $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) + $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count + + foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { + $arrayContainers += $container.Name + if ($container.Name -eq '$web' -and $staticWebsitesState) { + if ($storageAccount.properties.primaryEndpoints.web) { + try { + $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.properties.primaryEndpoints.web -Method 'HEAD' + $webSiteResponds = $true + } + catch { + $webSiteResponds = $false + } + } + } + + if ($container.Properties.PublicAccess) { + if ($container.Properties.PublicAccess -eq 'blob') { + $arrayContainersAnonymousBlob += $container.Name + } + if ($container.Properties.PublicAccess -eq 'container') { + $arrayContainersAnonymousContainer += $container.Name + } + } + } + } + } + + $allowSharedKeyAccess = $storageAccount.properties.allowSharedKeyAccess + if ([string]::IsNullOrWhiteSpace($storageAccount.properties.allowSharedKeyAccess)) { + $allowSharedKeyAccess = 'likely True' + } + $requireInfrastructureEncryption = $storageAccount.properties.encryption.requireInfrastructureEncryption + if ([string]::IsNullOrWhiteSpace($storageAccount.properties.encryption.requireInfrastructureEncryption)) { + $requireInfrastructureEncryption = 'likely False' + } + + $arrayResourceAccessRules = [System.Collections.ArrayList]@() + if ($storageAccount.properties.networkAcls.resourceAccessRules) { + if ($storageAccount.properties.networkAcls.resourceAccessRules.count -gt 0) { + foreach ($resourceAccessRule in $storageAccount.properties.networkAcls.resourceAccessRules) { + + $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' + $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" + + [regex]$regex = '\*+' + $resourceAccessRule.resourceId + switch ($regex.matches($resourceAccessRule.resourceId).count) { + { $_ -eq 1 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resourceGroup' + sort = 3 + }) + } + { $_ -eq 2 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'subscription' + sort = 2 + }) + } + { $_ -eq 3 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'tenant' + sort = 1 + }) + } + default { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resource' + resource = $resourceAccessRule.resourceId + sort = 0 + }) + } + } + } + } + } + $resourceAccessRulesCount = $arrayResourceAccessRules.count + if ($resourceAccessRulesCount -eq 0) { + $resourceAccessRules = '' + } + else { + $ht = @{} + foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { + + if ($accessRulePerRange.Name -eq 'resource') { + $arrayResources = @() + foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { + $arrayResources += $resource + } + $ht.($accessRulePerRange.Name) = [array]($arrayResources) + } + else { + $arrayResourceTypes = @() + foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { + $arrayResourceTypes += $resourceType + } + $ht.($accessRulePerRange.Name) = [array]($arrayResourceTypes) + } + } + $resourceAccessRules = $ht | ConvertTo-Json + } + + if ([string]::IsNullOrWhiteSpace($storageAccount.properties.publicNetworkAccess)) { + $publicNetworkAccess = 'likely enabled' + } + else { + $publicNetworkAccess = $storageAccount.properties.publicNetworkAccess + } + + $temp = [System.Collections.ArrayList]@() + $null = $temp.Add([PSCustomObject]@{ + storageAccount = $storageAccount.name + kind = $storageAccount.kind + skuName = $storageAccount.sku.name + skuTier = $storageAccount.sku.tier + location = $storageAccount.location + creationTime = $storageAccount.properties.creationTime + allowBlobPublicAccess = $storageAccount.properties.allowBlobPublicAccess + publicNetworkAccess = $publicNetworkAccess + SubscriptionId = $subscriptionId + SubscriptionName = $subDetails.displayName + subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId + subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' + resourceGroup = $resourceGroupName + networkAclsdefaultAction = $storageAccount.properties.networkAcls.defaultAction + staticWebsitesState = $staticWebsitesState + staticWebsitesResponse = $webSiteResponds + containersCanBeListed = $listContainersSuccess + containersCount = $containersCount + containers = $arrayContainers -join "$CSVDelimiterOpposite " + containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count + containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " + containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count + containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " + ipRulesCount = $storageAccount.properties.networkAcls.ipRules.Count + ipRulesIPAddressList = ($storageAccount.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " + virtualNetworkRulesCount = $storageAccount.properties.networkAcls.virtualNetworkRules.Count + virtualNetworkRulesList = ($storageAccount.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " + resourceAccessRulesCount = $resourceAccessRulesCount + resourceAccessRules = $resourceAccessRules + bypass = $storageAccount.properties.networkAcls.bypass -join "$CSVDelimiterOpposite " + supportsHttpsTrafficOnly = $storageAccount.properties.supportsHttpsTrafficOnly + minimumTlsVersion = $storageAccount.properties.minimumTlsVersion + allowSharedKeyAccess = $allowSharedKeyAccess + requireInfrastructureEncryption = $requireInfrastructureEncryption + }) + + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { + if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } + } + } + + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + if ($storageAccount.tags) { + $htAllSATags = @{} + foreach ($saTagName in ($storageAccount.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { + $htAllSATags.$saTagName = $storageAccount.tags.$saTagName + } + } + foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { + if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } + } + } + + $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) + + } -ThrottleLimit $ThrottleLimit + } + else { + Write-Host " No Storage Accounts present" + } + + $end = Get-Date + Write-Host " Processing Storage Account Analysis duration: $((NEW-TIMESPAN -Start $start -End $end).TotalSeconds) seconds" +} function processTenantSummary() { Write-Host ' Building TenantSummary' showMemoryUsage @@ -13290,7 +13581,7 @@ extensions: [{ name: 'sort' }] #region SUMMARYALZPolicies Write-Host ' processing TenantSummary ALZPolicies' - if (-not $NoALZEvergreen) { + if (-not $NoALZPolicyVersionChecker) { $alzPoliciesInTenant = [System.Collections.ArrayList]@() #policies @@ -13392,7 +13683,7 @@ extensions: [{ name: 'sort' }] $htmlTableId = 'TenantSummary_ALZPolicies' $abbrALZ = " " [void]$htmlTenantSummary.AppendLine(@" -
Azure Landing Zones (ALZ) GitHub
@@ -13416,9 +13707,9 @@ extensions: [{ name: 'sort' }] "@) - $htmlSUMMARYALZEverGreen = $null + $htmlSUMMARYALZPolicyVersionChecker = $null $exemptionData4CSVExport = [System.Collections.ArrayList]@() - $htmlSUMMARYALZEverGreen = foreach ($entry in $alzPoliciesInTenant) { + $htmlSUMMARYALZPolicyVersionChecker = foreach ($entry in $alzPoliciesInTenant) { if ([string]::IsNullOrWhiteSpace($entry.AzAdvertizerUrl)) { $link = '' } @@ -13443,11 +13734,11 @@ extensions: [{ name: 'sort' }] } if (-not $NoCsvExport) { - Write-Host "Exporting ALZ EverGreen CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZEverGreen.csv'" - $alzPoliciesInTenant | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZEverGreen.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + Write-Host "Exporting 'Azure Landing Zones (ALZ) Policy Version Checker' CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZPolicyVersionChecker.csv'" + $alzPoliciesInTenant | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZPolicyVersionChecker.csv" -Delimiter "$csvDelimiter" -NoTypeInformation } - [void]$htmlTenantSummary.AppendLine($htmlSUMMARYALZEverGreen) + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYALZPolicyVersionChecker) [void]$htmlTenantSummary.AppendLine(@" @@ -13509,13 +13800,13 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

Azure Landing Zones EverGreen

+

Azure Landing Zones (ALZ) Policy Version Checker

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

Azure Landing Zones EverGreen (parameter -NoALZEvergreen = $NoALZEvergreen)

+

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

"@) } #endregion SUMMARYALZPolicies @@ -19628,6 +19919,203 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { #endregion SUMMARYPSRule } + + #region SUMMARYStorageAccountAnalysis + if (1 -eq 1) { + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + $startStorageAccountAnalysis = Get-Date + Write-Host ' processing TenantSummary Storage Account Access Analysis' + + $arrayStorageAccountAnalysisResultsCount = $arrayStorageAccountAnalysisResults.Count + if ($arrayStorageAccountAnalysisResultsCount.Count -gt 0) { + + if (-not $NoCsvExport) { + $storageAccountAccessAnalysisCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_StorageAccountAccessAnalysis.csv" + Write-Host " Exporting 'Storage Account Access Analysis' CSV '$storageAccountAccessAnalysisCSVPath'" + $arrayStorageAccountAnalysisResults | Sort-Object -Property StorageAccount | Export-Csv -Path $storageAccountAccessAnalysisCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + } + + $htmlTableId = 'TenantSummary_StorageAccountAccessAnalysis' + $tfCount = $arrayStorageAccountAnalysisResultsCount + [void]$htmlTenantSummary.AppendLine(@" + +
+ Check this article by Elli Shlomo (MVP) Azure Blob Container Threats & Attacks
+ If you enabled the parameters StorageAccountAccessAnalysisSubscriptionTags or StorageAccountAccessAnalysisStorageAccountTags these are integrated in the CSV output *_StorageAccountAccessAnalysis.csv
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $arrayStorageAccountAnalysisResults | sort-Object -Property Name) { + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
StorageAccountkindskuNameskuTierlocationallowBlobPublicAccesspublicNetworkAccesssubscriptionMGPathresourceGroupnetworkAclsdefaultActionstaticWebsitesStatestaticWebsitesResponsecontainersCanBeListedcontainersCountcontainersAnonymousContainerCountcontainersAnonymousBlobCountipRulesCountipRulesIPAddressListvirtualNetworkRulesCountresourceAccessRulesCountresourceAccessRulesbypasssupportsHttpsTrafficOnlyminimumTlsVersionallowSharedKeyAccessrequireInfrastructureEncryption
$($result.storageAccount)$($result.kind)$($result.skuName)$($result.skuTier)$($result.location)$($result.allowBlobPublicAccess)$($result.publicNetworkAccess)$($result.subscriptionMGPath)$($result.resourceGroup)$($result.networkAclsdefaultAction)$($result.staticWebsitesState)$($result.staticWebsitesResponse)$($result.containersCanBeListed)$($result.containersCount)$($result.containersAnonymousContainerCount)$($result.containersAnonymousBlobCount)$($result.ipRulesCount)$($result.ipRulesIPAddressList)$($result.virtualNetworkRulesCount)$($result.resourceAccessRulesCount)$($result.resourceAccessRules)$($result.bypass)$($result.supportsHttpsTrafficOnly)$($result.minimumTlsVersion)$($result.allowSharedKeyAccess)$($result.requireInfrastructureEncryption)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Storage Accounts found

+'@) + } + $endStorageAccountAnalysis = Get-Date + Write-Host " Storage Account Analysis processing duration: $((NEW-TIMESPAN -Start $startStorageAccountAnalysis -End $endStorageAccountAnalysis).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startStorageAccountAnalysis -End $endStorageAccountAnalysis).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@" + Storage Account Access Analysis disabled - parameter -NoStorageAccountAccessAnalysis $($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis) +"@) + } +} + #endregion SUMMARYStorageAccountAnalysis + + [void]$htmlTenantSummary.AppendLine(@'
'@) @@ -24994,13 +25482,28 @@ function runInfo { #$script:paramsUsed += "NoDefinitionInsightsDedicatedHTML: $($NoDefinitionInsightsDedicatedHTML) " } - if ($NoALZEvergreen) { - Write-Host " NoALZEvergreen = $($NoALZEvergreen)" -ForegroundColor Green - #$script:paramsUsed += "NoALZEvergreen: $($NoALZEvergreen) " + if ($NoALZPolicyVersionChecker) { + Write-Host " NoALZPolicyVersionChecker = $($NoALZPolicyVersionChecker)" -ForegroundColor Green + #$script:paramsUsed += "NoALZPolicyVersionChecker: $($NoALZPolicyVersionChecker) " } else { - Write-Host " NoALZEvergreen = $($NoALZEvergreen)" -ForegroundColor Yellow - #$script:paramsUsed += "NoALZEvergreen: $($NoALZEvergreen) " + Write-Host " NoALZPolicyVersionChecker = $($NoALZPolicyVersionChecker)" -ForegroundColor Yellow + #$script:paramsUsed += "NoALZPolicyVersionChecker: $($NoALZPolicyVersionChecker) " + } + + if ($NoStorageAccountAccessAnalysis) { + Write-Host " NoStorageAccountAccessAnalysis = $($NoStorageAccountAccessAnalysis)" -ForegroundColor Green + #$script:paramsUsed += "NoStorageAccountAccessAnalysis: $($NoStorageAccountAccessAnalysis) " + } + else { + Write-Host " NoStorageAccountAccessAnalysis = $($NoStorageAccountAccessAnalysis)" -ForegroundColor Yellow + #$script:paramsUsed += "NoStorageAccountAccessAnalysis: $($NoStorageAccountAccessAnalysis) " + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + Write-Host " -StorageAccountAccessAnalysisSubscriptionTags: $($StorageAccountAccessAnalysisSubscriptionTags -join ', ')" -ForegroundColor Yellow + } + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + Write-Host " -StorageAccountAccessAnalysisStorageAccountTags: $($StorageAccountAccessAnalysisStorageAccountTags -join ', ')" -ForegroundColor Yellow + } } } @@ -25939,6 +26442,26 @@ function dataCollectionDiagnosticsMG { } $funcDataCollectionDiagnosticsMG = $function:dataCollectionDiagnosticsMG.ToString() +function dataCollectionStorageAccounts { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $ChildMgMgPath, + $ChildMgParentNameChainDelimited, + $subscriptionQuotaId + ) + + $currentTask = "Getting Storage Accounts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Storage/storageAccounts?api-version=2021-09-01" + $method = 'GET' + $storageAccountsSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + foreach ($storageAccount in $storageAccountsSubscriptionResult) { + $null = $script:storageAccounts.Add($storageAccount) + } +} +$funcDataCollectionStorageAccounts = $function:dataCollectionStorageAccounts.ToString() + function dataCollectionResources { [CmdletBinding()]Param( [string]$scopeId, @@ -27390,7 +27913,7 @@ function dataCollectionPolicyDefinitions { } - if ($azAPICallConf['htParameters'].NoALZEvergreen -eq $false) { + if ($azAPICallConf['htParameters'].NoALZPolicyVersionChecker -eq $false) { $policyJsonRule = $scopePolicyDefinition.properties.policyRule | ConvertTo-Json -depth 99 $hash = [System.Security.Cryptography.HashAlgorithm]::Create("sha256").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) @@ -27672,7 +28195,7 @@ function dataCollectionPolicySetDefinitions { $htTemp.ScopeMGLevel = $htSubscriptionsMgPath.((($scopePolicySetDefinition.Id).split('/'))[2]).level } - if ($azAPICallConf['htParameters'].NoALZEvergreen -eq $false) { + if ($azAPICallConf['htParameters'].NoALZPolicyVersionChecker -eq $false) { $policyJsonParameters = $scopePolicySetDefinition.properties.parameters | ConvertTo-Json -depth 99 $policyJsonPolicyDefinitions = $scopePolicySetDefinition.properties.policyDefinitions | ConvertTo-Json -depth 99 @@ -29941,6 +30464,7 @@ if ($DoPSRule) { ModulePathPipeline = 'PSRuleModule' }) } + verifyModules3rd -modules $modules #endregion verifyModules3rd @@ -29971,7 +30495,6 @@ if ($azGovVizNewerVersionAvailable) { handleCloudEnvironment - if (-not $HierarchyMapOnly) { #region recommendPSRule if (-not $azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) { @@ -30132,32 +30655,34 @@ if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { $alzPolicySetHashes = @{} $htDoARMRoleAssignmentScheduleInstances = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} $htDoARMRoleAssignmentScheduleInstances.Do = $true + $storageAccounts = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $arrayStorageAccountAnalysisResults = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) } if (-not $HierarchyMapOnly) { - if (-not $NoALZEvergreen) { + if (-not $NoALZPolicyVersionChecker) { switch ($azAPICallConf['checkContext'].Environment.Name) { 'Azurecloud' { - Write-Host "ALZ EverGreen feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - processALZEverGreen + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + processALZPolicyVersionChecker } 'AzureChinaCloud' { - Write-Host "ALZ EverGreen feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - processALZEverGreen + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + processALZPolicyVersionChecker } 'AzureUSGovernment' { - Write-Host "ALZ EverGreen feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - processALZEverGreen + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + processALZPolicyVersionChecker } Default { - Write-Host "ALZ EverGreen feature NOT supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - Write-Host "Setting parameter -NoALZEvergreen to 'true'" - $NoALZEvergreen = $true + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature NOT supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + Write-Host "Setting parameter -NoALZPolicyVersionChecker to 'true'" + $NoALZPolicyVersionChecker = $true } } } else { - #Write-Host "Skipping ALZ EverGreen (parameter -NoALZEvergreen = $NoALZEvergreen)" + #Write-Host "Skipping 'Azure Landing Zones (ALZ) Policy Version Checker' (parameter -NoALZPolicyVersionChecker = $NoALZPolicyVersionChecker)" } } @@ -30235,6 +30760,11 @@ if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { createTagList showMemoryUsage + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + processStorageAccountAnalysis + showMemoryUsage + } + if ($azAPICallConf['htParameters'].NoResources -eq $false) { getResourceDiagnosticsCapability showMemoryUsage diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index 934700f0..824235d5 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -152,15 +152,28 @@ Prevent integration of PIM eligible assignments with RoleAssignmentsAll (HTML, CSV) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoPIMEligibilityIntegrationRoleAssignmentsAll -.PARAMETER NoALZEvergreen - ALZ EverGreen - Azure Landing Zones EverGreen for Policy and Set definitions. AzGovViz will clone the ALZ GitHub repository and collect the ALZ policy and set definitions history. The ALZ data will be compared with the data from your tenant so that you can get lifecycle management recommendations for ALZ policy and set definitions that already exist in your tenant plus a list of ALZ policy and set definitions that do not exist in your tenant. The ALZ EverGreen results will be displayed in the TenantSummary and a CSV export `*_ALZEverGreen.csv` will be provided. - If you do not want to execute the ALZ EverGreen feature then use this parameter - PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZEvergreen +.PARAMETER NoALZPolicyVersionChecker + 'Azure Landing Zones (ALZ) Policy Version Checker' for Policy and Set definitions. AzGovViz will clone the ALZ GitHub repository and collect the ALZ policy and set definitions history. The ALZ data will be compared with the data from your tenant so that you can get lifecycle management recommendations for ALZ policy and set definitions that already exist in your tenant plus a list of ALZ policy and set definitions that do not exist in your tenant. The 'Azure Landing Zones (ALZ) Policy Version Checker' results will be displayed in the TenantSummary and a CSV export `*_ALZPolicyVersionChecker.csv` will be provided. + If you do not want to execute the 'Azure Landing Zones (ALZ) Policy Version Checker' feature then use this parameter + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZPolicyVersionChecker .PARAMETER NoDefinitionInsightsDedicatedHTML DefinitionInsights will be written to a separate HTML file `*_DefinitionInsights.html`. If you want to keep DefinitionInsights in the main html file then use this parameter PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoDefinitionInsightsDedicatedHTML +.PARAMETER NoStorageAccountAccessAnalysis + Analysis on Storage Accounts, specially focused on anonymous access. + If you do not want to execute this feature then use this parameter + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoStorageAccountAccessAnalysis + +.PARAMETER StorageAccountAccessAnalysisSubscriptionTags + If the Storage Account Access Analysis feature is executed with this parameter you can define the subscription tags that should be added to the CSV output + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -StorageAccountAccessAnalysisSubscriptionTags @('Responsible', 'TeamEmail') + +.PARAMETER StorageAccountAccessAnalysisStorageAccountTags + If the Storage Account Access Analysis feature is executed with this parameter you can define the Storage Account (resource) tags that should be added to the CSV output + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -StorageAccountAccessAnalysisStorageAccountTags @('SAResponsible', 'DataOfficer') + .EXAMPLE Define the ManagementGroup ID PS C:\> .\AzGovVizParallel.ps1 -ManagementGroupId @@ -297,12 +310,17 @@ Define if PIM Eligible assignments should not be integrated with RoleAssignmentsAll outputs (HTML, CSV) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoPIMEligibilityIntegrationRoleAssignmentsAll - Define if the ALZ EverGreen feature should not be executed - PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZEvergreen + Define if the 'Azure Landing Zones (ALZ) Policy Version Checker' feature should not be executed + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoALZPolicyVersionChecker Define if DefinitionInsights should not be written to a seperate html file (*_DefinitionInsights.html) PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoDefinitionInsightsDedicatedHTML + Define if Storage Account Access Analysis (focus on anonymous access) should be executed + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoStorageAccountAccessAnalysis + Additionally you can define Subscription and/or Storage Account Tag names that should be added to the CSV output per Storage Account + PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId --StorageAccountAccessAnalysisSubscriptionTags @('Responsible', 'TeamEmail') -StorageAccountAccessAnalysisStorageAccountTags @('SAResponsible', 'DataOfficer') + .NOTES AUTHOR: Julian Hayward - Customer Engineer - Customer Success Unit | Azure Infrastucture/Automation/Devops/Governance | Microsoft @@ -319,10 +337,10 @@ Param $Product = 'AzGovViz', [string] - $AzAPICallVersion = '1.1.23', + $AzAPICallVersion = '1.1.24', [string] - $ProductVersion = 'v6_major_20220917_1', + $ProductVersion = 'v6_major_20220927_1', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -489,11 +507,20 @@ Param $NoPIMEligibilityIntegrationRoleAssignmentsAll, [switch] - $NoALZEvergreen, + $NoALZPolicyVersionChecker, [switch] $NoDefinitionInsightsDedicatedHTML, + [switch] + $NoStorageAccountAccessAnalysis, + + [array] + $StorageAccountAccessAnalysisSubscriptionTags = @('undefined'), + + [array] + $StorageAccountAccessAnalysisStorageAccountTags = @('undefined'), + #https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits [int] $LimitRBACCustomRoleDefinitionsTenant = 5000, @@ -555,7 +582,8 @@ if ($ManagementGroupId -match " ") { } #region Functions -. ".\$($ScriptPath)\functions\processALZEverGreen.ps1" +. ".\$($ScriptPath)\functions\processStorageAccountAnalysis.ps1" +. ".\$($ScriptPath)\functions\processALZPolicyVersionChecker.ps1" . ".\$($ScriptPath)\functions\getPIMEligible.ps1" . ".\$($ScriptPath)\functions\testGuid.ps1" . ".\$($ScriptPath)\functions\apiCallTracking.ps1" @@ -651,6 +679,7 @@ if ($DoPSRule) { ModulePathPipeline = 'PSRuleModule' }) } + verifyModules3rd -modules $modules #endregion verifyModules3rd @@ -681,7 +710,6 @@ if ($azGovVizNewerVersionAvailable) { handleCloudEnvironment - if (-not $HierarchyMapOnly) { #region recommendPSRule if (-not $azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) { @@ -842,32 +870,34 @@ if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { $alzPolicySetHashes = @{} $htDoARMRoleAssignmentScheduleInstances = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} $htDoARMRoleAssignmentScheduleInstances.Do = $true + $storageAccounts = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $arrayStorageAccountAnalysisResults = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) } if (-not $HierarchyMapOnly) { - if (-not $NoALZEvergreen) { + if (-not $NoALZPolicyVersionChecker) { switch ($azAPICallConf['checkContext'].Environment.Name) { 'Azurecloud' { - Write-Host "ALZ EverGreen feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - processALZEverGreen + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + processALZPolicyVersionChecker } 'AzureChinaCloud' { - Write-Host "ALZ EverGreen feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - processALZEverGreen + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + processALZPolicyVersionChecker } 'AzureUSGovernment' { - Write-Host "ALZ EverGreen feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - processALZEverGreen + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + processALZPolicyVersionChecker } Default { - Write-Host "ALZ EverGreen feature NOT supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" - Write-Host "Setting parameter -NoALZEvergreen to 'true'" - $NoALZEvergreen = $true + Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature NOT supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'" + Write-Host "Setting parameter -NoALZPolicyVersionChecker to 'true'" + $NoALZPolicyVersionChecker = $true } } } else { - #Write-Host "Skipping ALZ EverGreen (parameter -NoALZEvergreen = $NoALZEvergreen)" + #Write-Host "Skipping 'Azure Landing Zones (ALZ) Policy Version Checker' (parameter -NoALZPolicyVersionChecker = $NoALZPolicyVersionChecker)" } } @@ -945,6 +975,11 @@ if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { createTagList showMemoryUsage + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + processStorageAccountAnalysis + showMemoryUsage + } + if ($azAPICallConf['htParameters'].NoResources -eq $false) { getResourceDiagnosticsCapability showMemoryUsage diff --git a/pwsh/dev/functions/addHtParameters.ps1 b/pwsh/dev/functions/addHtParameters.ps1 index 939c1d0a..c34be509 100644 --- a/pwsh/dev/functions/addHtParameters.ps1 +++ b/pwsh/dev/functions/addHtParameters.ps1 @@ -33,7 +33,8 @@ function addHtParameters { RBACAtScopeOnly = [bool]$RBACAtScopeOnly DoPSRule = [bool]$DoPSRule PSRuleFailedOnly = [bool]$PSRuleFailedOnly - NoALZEvergreen = [bool]$NoALZEvergreen + NoALZPolicyVersionChecker = [bool]$NoALZPolicyVersionChecker + NoStorageAccountAccessAnalysis = [bool]$NoStorageAccountAccessAnalysis } Write-Host 'htParameters:' $azAPICallConf['htParameters'] | format-table -AutoSize | Out-String diff --git a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 index e8d5435d..3d02daec 100644 --- a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 +++ b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 @@ -261,6 +261,26 @@ function dataCollectionDiagnosticsMG { } $funcDataCollectionDiagnosticsMG = $function:dataCollectionDiagnosticsMG.ToString() +function dataCollectionStorageAccounts { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $ChildMgMgPath, + $ChildMgParentNameChainDelimited, + $subscriptionQuotaId + ) + + $currentTask = "Getting Storage Accounts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Storage/storageAccounts?api-version=2021-09-01" + $method = 'GET' + $storageAccountsSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + foreach ($storageAccount in $storageAccountsSubscriptionResult) { + $null = $script:storageAccounts.Add($storageAccount) + } +} +$funcDataCollectionStorageAccounts = $function:dataCollectionStorageAccounts.ToString() + function dataCollectionResources { [CmdletBinding()]Param( [string]$scopeId, @@ -1712,7 +1732,7 @@ function dataCollectionPolicyDefinitions { } - if ($azAPICallConf['htParameters'].NoALZEvergreen -eq $false) { + if ($azAPICallConf['htParameters'].NoALZPolicyVersionChecker -eq $false) { $policyJsonRule = $scopePolicyDefinition.properties.policyRule | ConvertTo-Json -depth 99 $hash = [System.Security.Cryptography.HashAlgorithm]::Create("sha256").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) @@ -1994,7 +2014,7 @@ function dataCollectionPolicySetDefinitions { $htTemp.ScopeMGLevel = $htSubscriptionsMgPath.((($scopePolicySetDefinition.Id).split('/'))[2]).level } - if ($azAPICallConf['htParameters'].NoALZEvergreen -eq $false) { + if ($azAPICallConf['htParameters'].NoALZPolicyVersionChecker -eq $false) { $policyJsonParameters = $scopePolicySetDefinition.properties.parameters | ConvertTo-Json -depth 99 $policyJsonPolicyDefinitions = $scopePolicySetDefinition.properties.policyDefinitions | ConvertTo-Json -depth 99 diff --git a/pwsh/dev/functions/processALZEverGreen.ps1 b/pwsh/dev/functions/processALZPolicyVersionChecker.ps1 similarity index 98% rename from pwsh/dev/functions/processALZEverGreen.ps1 rename to pwsh/dev/functions/processALZPolicyVersionChecker.ps1 index bcaf9734..3e73d2b3 100644 --- a/pwsh/dev/functions/processALZEverGreen.ps1 +++ b/pwsh/dev/functions/processALZPolicyVersionChecker.ps1 @@ -1,6 +1,6 @@ -function processALZEverGreen { +function processALZPolicyVersionChecker { $start = get-date - Write-Host "Processing ALZ EverGreen base data" + Write-Host "Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data" $ALZRepositoryURI = 'https://github.com/Azure/Enterprise-Scale.git' $workingPath = Get-Location Write-Host " Working directory is '$($workingPath)'" @@ -26,9 +26,9 @@ function processALZEverGreen { if (-not (Test-Path -LiteralPath "$($ALZPath)/Enterprise-Scale" -PathType Container)) { $ALZCloneSuccess = $false Write-Host " Cloning '$($ALZRepositoryURI)' failed" - Write-Host " Setting switch parameter '-NoALZEvergreen' to true" - $script:NoALZEvergreen = $true - $script:azAPICallConf['htParameters'].NoALZEvergreen = $true + Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" + $script:NoALZPolicyVersionChecker = $true + $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true Write-Host " Switching back to working directory '$($workingPath)'" Set-Location $workingPath } @@ -40,9 +40,9 @@ function processALZEverGreen { catch { $_ Write-Host " Cloning '$($ALZRepositoryURI)' failed" - Write-Host " Setting switch parameter '-NoALZEvergreen' to true" - $script:NoALZEvergreen = $true - $script:azAPICallConf['htParameters'].NoALZEvergreen = $true + Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" + $script:NoALZPolicyVersionChecker = $true + $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true Write-Host " Switching back to working directory '$($workingPath)'" Set-Location $workingPath } @@ -569,5 +569,5 @@ function processALZEverGreen { } $end = Get-Date - Write-Host " Processing ALZ EverGreen base data duration: $((NEW-TIMESPAN -Start $start -End $end).TotalSeconds) seconds" + Write-Host " Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data duration: $((NEW-TIMESPAN -Start $start -End $end).TotalSeconds) seconds" } \ No newline at end of file diff --git a/pwsh/dev/functions/processDataCollection.ps1 b/pwsh/dev/functions/processDataCollection.ps1 index af60c1e7..21590b29 100644 --- a/pwsh/dev/functions/processDataCollection.ps1 +++ b/pwsh/dev/functions/processDataCollection.ps1 @@ -295,6 +295,7 @@ function processDataCollection { $scriptPath = $using:ScriptPath #Array&HTs $newTable = $using:newTable + $storageAccounts = $using:storageAccounts $resourcesAll = $using:resourcesAll $resourcesIdsAll = $using:resourcesIdsAll $resourceGroupsAll = $using:resourceGroupsAll @@ -360,6 +361,7 @@ function processDataCollection { $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub $function:dataCollectionResources = $using:funcDataCollectionResources + $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders $function:dataCollectionFeatures = $using:funcDataCollectionFeatures @@ -435,6 +437,14 @@ function processDataCollection { } DataCollectionDiagnosticsSub @baseParameters @dataCollectionDiagnosticsSubParameters + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + #resources + $dataCollectionStorageAccountsParameters = @{ + ChildMgMgPath = $childMgMgPath + ChildMgParentNameChainDelimited = $childMgParentNameChainDelimited + } + DataCollectionStorageAccounts @baseParameters @dataCollectionStorageAccountsParameters + } if ($azAPICallConf['htParameters'].NoResources -eq $false) { #resources diff --git a/pwsh/dev/functions/processStorageAccountAnalysis.ps1 b/pwsh/dev/functions/processStorageAccountAnalysis.ps1 new file mode 100644 index 00000000..9064a5a1 --- /dev/null +++ b/pwsh/dev/functions/processStorageAccountAnalysis.ps1 @@ -0,0 +1,253 @@ +function processStorageAccountAnalysis { + $start = get-date + Write-Host "Processing Storage Account Analysis" + $storageAccountscount = $storageAccounts.count + if ($storageAccountscount -gt 0) { + Write-Host " Executing Storage Account Analysis for $storageAccountscount Storage Accounts" + $script:arrayStorageAccountAnalysisResults = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + + $storageAccounts | ForEach-Object -Parallel { + $storageAccount = $_ + $azAPICallConf = $using:azAPICallConf + $arrayStorageAccountAnalysisResults = $using:arrayStorageAccountAnalysisResults + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htSubscriptionTags = $using:htSubscriptionTags + $CSVDelimiterOpposite = $using:CSVDelimiterOpposite + $StorageAccountAccessAnalysisSubscriptionTags = $using:StorageAccountAccessAnalysisSubscriptionTags + $StorageAccountAccessAnalysisStorageAccountTags = $using:StorageAccountAccessAnalysisStorageAccountTags + $listContainersSuccess = 'n/a' + $containersCount = 'n/a' + $arrayContainers = @() + $arrayContainersAnonymousContainer = @() + $arrayContainersAnonymousBlob = @() + $staticWebsitesState = 'n/a' + $webSiteResponds = 'n/a' + + $subscriptionId = ($storageAccount.id -split '/')[2] + $resourceGroupName = ($storageAccount.id -split '/')[4] + $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails + + Write-Host "Processing SA; Subscription: $($subDetails.displayName) ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)] - Storage Account: $($storageAccount.name)" + + if ($storageAccount.Properties.primaryEndpoints.blob) { + + $urlServiceProps = "$($storageAccount.Properties.primaryEndpoints.blob)?restype=service&comp=properties" + $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.name) get restype=service&comp=properties" + try { + $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) + } + catch { + Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.name)" + $saProperties | ConvertTo-Json -Depth 99 + } + + $staticWebsitesState = $false + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { + $staticWebsitesState = $true + } + } + + $urlCompList = "$($storageAccount.Properties.primaryEndpoints.blob)?comp=list" + $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.name) get comp=list" + if ($listContainers -eq 'AuthorizationFailure') { + $listContainersSuccess = $false + } + else { + $listContainersSuccess = $true + } + + if ($listContainersSuccess) { + $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) + $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count + + foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { + $arrayContainers += $container.Name + if ($container.Name -eq '$web' -and $staticWebsitesState) { + if ($storageAccount.properties.primaryEndpoints.web) { + try { + $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.properties.primaryEndpoints.web -Method 'HEAD' + $webSiteResponds = $true + } + catch { + $webSiteResponds = $false + } + } + } + + if ($container.Properties.PublicAccess) { + if ($container.Properties.PublicAccess -eq 'blob') { + $arrayContainersAnonymousBlob += $container.Name + } + if ($container.Properties.PublicAccess -eq 'container') { + $arrayContainersAnonymousContainer += $container.Name + } + } + } + } + } + + $allowSharedKeyAccess = $storageAccount.properties.allowSharedKeyAccess + if ([string]::IsNullOrWhiteSpace($storageAccount.properties.allowSharedKeyAccess)) { + $allowSharedKeyAccess = 'likely True' + } + $requireInfrastructureEncryption = $storageAccount.properties.encryption.requireInfrastructureEncryption + if ([string]::IsNullOrWhiteSpace($storageAccount.properties.encryption.requireInfrastructureEncryption)) { + $requireInfrastructureEncryption = 'likely False' + } + + $arrayResourceAccessRules = [System.Collections.ArrayList]@() + if ($storageAccount.properties.networkAcls.resourceAccessRules) { + if ($storageAccount.properties.networkAcls.resourceAccessRules.count -gt 0) { + foreach ($resourceAccessRule in $storageAccount.properties.networkAcls.resourceAccessRules) { + + $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' + $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" + + [regex]$regex = '\*+' + $resourceAccessRule.resourceId + switch ($regex.matches($resourceAccessRule.resourceId).count) { + { $_ -eq 1 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resourceGroup' + sort = 3 + }) + } + { $_ -eq 2 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'subscription' + sort = 2 + }) + } + { $_ -eq 3 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'tenant' + sort = 1 + }) + } + default { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resource' + resource = $resourceAccessRule.resourceId + sort = 0 + }) + } + } + } + } + } + $resourceAccessRulesCount = $arrayResourceAccessRules.count + if ($resourceAccessRulesCount -eq 0) { + $resourceAccessRules = '' + } + else { + $ht = @{} + foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { + + if ($accessRulePerRange.Name -eq 'resource') { + $arrayResources = @() + foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { + $arrayResources += $resource + } + $ht.($accessRulePerRange.Name) = [array]($arrayResources) + } + else { + $arrayResourceTypes = @() + foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { + $arrayResourceTypes += $resourceType + } + $ht.($accessRulePerRange.Name) = [array]($arrayResourceTypes) + } + } + $resourceAccessRules = $ht | ConvertTo-Json + } + + if ([string]::IsNullOrWhiteSpace($storageAccount.properties.publicNetworkAccess)) { + $publicNetworkAccess = 'likely enabled' + } + else { + $publicNetworkAccess = $storageAccount.properties.publicNetworkAccess + } + + $temp = [System.Collections.ArrayList]@() + $null = $temp.Add([PSCustomObject]@{ + storageAccount = $storageAccount.name + kind = $storageAccount.kind + skuName = $storageAccount.sku.name + skuTier = $storageAccount.sku.tier + location = $storageAccount.location + creationTime = $storageAccount.properties.creationTime + allowBlobPublicAccess = $storageAccount.properties.allowBlobPublicAccess + publicNetworkAccess = $publicNetworkAccess + SubscriptionId = $subscriptionId + SubscriptionName = $subDetails.displayName + subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId + subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' + resourceGroup = $resourceGroupName + networkAclsdefaultAction = $storageAccount.properties.networkAcls.defaultAction + staticWebsitesState = $staticWebsitesState + staticWebsitesResponse = $webSiteResponds + containersCanBeListed = $listContainersSuccess + containersCount = $containersCount + containers = $arrayContainers -join "$CSVDelimiterOpposite " + containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count + containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " + containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count + containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " + ipRulesCount = $storageAccount.properties.networkAcls.ipRules.Count + ipRulesIPAddressList = ($storageAccount.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " + virtualNetworkRulesCount = $storageAccount.properties.networkAcls.virtualNetworkRules.Count + virtualNetworkRulesList = ($storageAccount.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " + resourceAccessRulesCount = $resourceAccessRulesCount + resourceAccessRules = $resourceAccessRules + bypass = $storageAccount.properties.networkAcls.bypass -join "$CSVDelimiterOpposite " + supportsHttpsTrafficOnly = $storageAccount.properties.supportsHttpsTrafficOnly + minimumTlsVersion = $storageAccount.properties.minimumTlsVersion + allowSharedKeyAccess = $allowSharedKeyAccess + requireInfrastructureEncryption = $requireInfrastructureEncryption + }) + + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { + if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } + } + } + + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + if ($storageAccount.tags) { + $htAllSATags = @{} + foreach ($saTagName in ($storageAccount.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { + $htAllSATags.$saTagName = $storageAccount.tags.$saTagName + } + } + foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { + if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } + } + } + + $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) + + } -ThrottleLimit $ThrottleLimit + } + else { + Write-Host " No Storage Accounts present" + } + + $end = Get-Date + Write-Host " Processing Storage Account Analysis duration: $((NEW-TIMESPAN -Start $start -End $end).TotalSeconds) seconds" +} \ No newline at end of file diff --git a/pwsh/dev/functions/processTenantSummary.ps1 b/pwsh/dev/functions/processTenantSummary.ps1 index 397b1fe6..82a99647 100644 --- a/pwsh/dev/functions/processTenantSummary.ps1 +++ b/pwsh/dev/functions/processTenantSummary.ps1 @@ -2368,7 +2368,7 @@ extensions: [{ name: 'sort' }] #region SUMMARYALZPolicies Write-Host ' processing TenantSummary ALZPolicies' - if (-not $NoALZEvergreen) { + if (-not $NoALZPolicyVersionChecker) { $alzPoliciesInTenant = [System.Collections.ArrayList]@() #policies @@ -2470,7 +2470,7 @@ extensions: [{ name: 'sort' }] $htmlTableId = 'TenantSummary_ALZPolicies' $abbrALZ = " " [void]$htmlTenantSummary.AppendLine(@" -
Azure Landing Zones (ALZ) GitHub
@@ -2494,9 +2494,9 @@ extensions: [{ name: 'sort' }] "@) - $htmlSUMMARYALZEverGreen = $null + $htmlSUMMARYALZPolicyVersionChecker = $null $exemptionData4CSVExport = [System.Collections.ArrayList]@() - $htmlSUMMARYALZEverGreen = foreach ($entry in $alzPoliciesInTenant) { + $htmlSUMMARYALZPolicyVersionChecker = foreach ($entry in $alzPoliciesInTenant) { if ([string]::IsNullOrWhiteSpace($entry.AzAdvertizerUrl)) { $link = '' } @@ -2521,11 +2521,11 @@ extensions: [{ name: 'sort' }] } if (-not $NoCsvExport) { - Write-Host "Exporting ALZ EverGreen CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZEverGreen.csv'" - $alzPoliciesInTenant | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZEverGreen.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + Write-Host "Exporting 'Azure Landing Zones (ALZ) Policy Version Checker' CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZPolicyVersionChecker.csv'" + $alzPoliciesInTenant | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZPolicyVersionChecker.csv" -Delimiter "$csvDelimiter" -NoTypeInformation } - [void]$htmlTenantSummary.AppendLine($htmlSUMMARYALZEverGreen) + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYALZPolicyVersionChecker) [void]$htmlTenantSummary.AppendLine(@" @@ -2587,13 +2587,13 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

Azure Landing Zones EverGreen

+

Azure Landing Zones (ALZ) Policy Version Checker

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

Azure Landing Zones EverGreen (parameter -NoALZEvergreen = $NoALZEvergreen)

+

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

"@) } #endregion SUMMARYALZPolicies @@ -8706,6 +8706,203 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { #endregion SUMMARYPSRule } + + #region SUMMARYStorageAccountAnalysis + if (1 -eq 1) { + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + $startStorageAccountAnalysis = Get-Date + Write-Host ' processing TenantSummary Storage Account Access Analysis' + + $arrayStorageAccountAnalysisResultsCount = $arrayStorageAccountAnalysisResults.Count + if ($arrayStorageAccountAnalysisResultsCount.Count -gt 0) { + + if (-not $NoCsvExport) { + $storageAccountAccessAnalysisCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_StorageAccountAccessAnalysis.csv" + Write-Host " Exporting 'Storage Account Access Analysis' CSV '$storageAccountAccessAnalysisCSVPath'" + $arrayStorageAccountAnalysisResults | Sort-Object -Property StorageAccount | Export-Csv -Path $storageAccountAccessAnalysisCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + } + + $htmlTableId = 'TenantSummary_StorageAccountAccessAnalysis' + $tfCount = $arrayStorageAccountAnalysisResultsCount + [void]$htmlTenantSummary.AppendLine(@" + +
+ Check this article by Elli Shlomo (MVP) Azure Blob Container Threats & Attacks
+ If you enabled the parameters StorageAccountAccessAnalysisSubscriptionTags or StorageAccountAccessAnalysisStorageAccountTags these are integrated in the CSV output *_StorageAccountAccessAnalysis.csv
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $arrayStorageAccountAnalysisResults | sort-Object -Property Name) { + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
StorageAccountkindskuNameskuTierlocationallowBlobPublicAccesspublicNetworkAccesssubscriptionMGPathresourceGroupnetworkAclsdefaultActionstaticWebsitesStatestaticWebsitesResponsecontainersCanBeListedcontainersCountcontainersAnonymousContainerCountcontainersAnonymousBlobCountipRulesCountipRulesIPAddressListvirtualNetworkRulesCountresourceAccessRulesCountresourceAccessRulesbypasssupportsHttpsTrafficOnlyminimumTlsVersionallowSharedKeyAccessrequireInfrastructureEncryption
$($result.storageAccount)$($result.kind)$($result.skuName)$($result.skuTier)$($result.location)$($result.allowBlobPublicAccess)$($result.publicNetworkAccess)$($result.subscriptionMGPath)$($result.resourceGroup)$($result.networkAclsdefaultAction)$($result.staticWebsitesState)$($result.staticWebsitesResponse)$($result.containersCanBeListed)$($result.containersCount)$($result.containersAnonymousContainerCount)$($result.containersAnonymousBlobCount)$($result.ipRulesCount)$($result.ipRulesIPAddressList)$($result.virtualNetworkRulesCount)$($result.resourceAccessRulesCount)$($result.resourceAccessRules)$($result.bypass)$($result.supportsHttpsTrafficOnly)$($result.minimumTlsVersion)$($result.allowSharedKeyAccess)$($result.requireInfrastructureEncryption)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Storage Accounts found

+'@) + } + $endStorageAccountAnalysis = Get-Date + Write-Host " Storage Account Analysis processing duration: $((NEW-TIMESPAN -Start $startStorageAccountAnalysis -End $endStorageAccountAnalysis).TotalMinutes) minutes ($((NEW-TIMESPAN -Start $startStorageAccountAnalysis -End $endStorageAccountAnalysis).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@" + Storage Account Access Analysis disabled - parameter -NoStorageAccountAccessAnalysis $($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis) +"@) + } +} + #endregion SUMMARYStorageAccountAnalysis + + [void]$htmlTenantSummary.AppendLine(@'
'@) diff --git a/pwsh/dev/functions/runInfo.ps1 b/pwsh/dev/functions/runInfo.ps1 index 8d58ce18..d065c7e7 100644 --- a/pwsh/dev/functions/runInfo.ps1 +++ b/pwsh/dev/functions/runInfo.ps1 @@ -397,13 +397,28 @@ function runInfo { #$script:paramsUsed += "NoDefinitionInsightsDedicatedHTML: $($NoDefinitionInsightsDedicatedHTML) " } - if ($NoALZEvergreen) { - Write-Host " NoALZEvergreen = $($NoALZEvergreen)" -ForegroundColor Green - #$script:paramsUsed += "NoALZEvergreen: $($NoALZEvergreen) " + if ($NoALZPolicyVersionChecker) { + Write-Host " NoALZPolicyVersionChecker = $($NoALZPolicyVersionChecker)" -ForegroundColor Green + #$script:paramsUsed += "NoALZPolicyVersionChecker: $($NoALZPolicyVersionChecker) " } else { - Write-Host " NoALZEvergreen = $($NoALZEvergreen)" -ForegroundColor Yellow - #$script:paramsUsed += "NoALZEvergreen: $($NoALZEvergreen) " + Write-Host " NoALZPolicyVersionChecker = $($NoALZPolicyVersionChecker)" -ForegroundColor Yellow + #$script:paramsUsed += "NoALZPolicyVersionChecker: $($NoALZPolicyVersionChecker) " + } + + if ($NoStorageAccountAccessAnalysis) { + Write-Host " NoStorageAccountAccessAnalysis = $($NoStorageAccountAccessAnalysis)" -ForegroundColor Green + #$script:paramsUsed += "NoStorageAccountAccessAnalysis: $($NoStorageAccountAccessAnalysis) " + } + else { + Write-Host " NoStorageAccountAccessAnalysis = $($NoStorageAccountAccessAnalysis)" -ForegroundColor Yellow + #$script:paramsUsed += "NoStorageAccountAccessAnalysis: $($NoStorageAccountAccessAnalysis) " + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + Write-Host " -StorageAccountAccessAnalysisSubscriptionTags: $($StorageAccountAccessAnalysisSubscriptionTags -join ', ')" -ForegroundColor Yellow + } + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + Write-Host " -StorageAccountAccessAnalysisStorageAccountTags: $($StorageAccountAccessAnalysisStorageAccountTags -join ', ')" -ForegroundColor Yellow + } } } diff --git a/version.txt b/version.txt index 936c0f46..e1d2edac 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v6_major_20220917_1 \ No newline at end of file +v6_major_20220927_1 \ No newline at end of file