diff --git a/README.md b/README.md index 8f2acd25..8ca62b54 100644 --- a/README.md +++ b/README.md @@ -84,13 +84,17 @@ The [Azure Governance Visualizer accelerator](https://github.com/Azure/Azure-Gov ## Release history +**Changes** (2024-October-26 / 6.6.0 Minor) + +- Microsoft Defender for Cloud Coverage (Tenant Summary and CSV export). Example html: +![MicrosoftDefenderForCloudCoverage_preview](img/MicrosoftDefenderForCloudCoverage_preview.png) +- CostOptimization add `microsoft.network/privateendpoints` for intent=cost savings +- extend ResourcesAll.csv output with sku and kind information +- update [API reference](#api-reference) '/subscriptions/`subscriptionId`/resources' use API version 2024-03-01 (previous 2023-07-01) + **Changes** (2024-October-9 / 6.5.5 Patch) - introduce a new optional [parameter](#parameters) `-SubscriptionIdWhitelist`, which defines the subscriptions that must match in order to be processed. - -**Changes** (2024-September-19 / 6.5.4 Patch) - -- minor PSScriptAnalyzer finding resolved [Full release history](history.md) @@ -609,7 +613,7 @@ Azure Governance Visualizer polls the following APIs | ARM | 2020-01-01-preview | /subscriptions/`subscriptionId`/providers/Microsoft.Security/securityContacts | | ARM | 2019-10-01 | /subscriptions/`subscriptionId`/providers | | ARM | 2021-04-01 | /subscriptions/`subscriptionId`/resourcegroups | -| ARM | 2023-07-01 | /subscriptions/`subscriptionId`/resources | +| ARM | 2024-03-01 | /subscriptions/`subscriptionId`/resources | | ARM | 2020-01-01 | /subscriptions | | ARM | 2020-01-01 | /tenants | diff --git a/history.md b/history.md index 7a0367ac..96232ddd 100644 --- a/history.md +++ b/history.md @@ -4,6 +4,14 @@ ### Azure Governance Visualizer version 6 +**Changes** (2024-October-26 / 6.6.0 Minor) + +- Microsoft Defender for Cloud Coverage (Tenant Summary and CSV export) +![MicrosoftDefenderForCloudCoverage_preview](img/MicrosoftDefenderForCloudCoverage_preview.png) +- CostOptimization add `microsoft.network/privateendpoints` for intent=cost savings +- extend ResourcesAll.csv output with sku and kind information +- update [API reference](#api-reference) '/subscriptions/`subscriptionId`/resources' use API version 2024-03-01 (previous 2023-07-01) + **Changes** (2024-October-9 / 6.5.5 Patch) - introduce a new optional parameter `-SubscriptionIdWhitelist`, which defines the subscriptions that must match in order to be processed. diff --git a/img/MicrosoftDefenderForCloudCoverage_preview.png b/img/MicrosoftDefenderForCloudCoverage_preview.png new file mode 100644 index 00000000..9de356f0 Binary files /dev/null and b/img/MicrosoftDefenderForCloudCoverage_preview.png differ diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index e15a9638..758ef38b 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -371,7 +371,7 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.5.5', + $ProductVersion = '6.6.0', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -4477,7 +4477,7 @@ resources intent = $intent }) - $intent = 'misconfiguration' + $intent = 'cost savings' $null = $queries.Add([PSCustomObject]@{ queryName = 'microsoft.network/privateendpoints' query = @" @@ -6667,6 +6667,7 @@ function processDataCollection { $htUserTypesGuest = $using:htUserTypesGuest $arrayDefenderPlans = $using:arrayDefenderPlans $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped + $htSecuritySettings = $using:htSecuritySettings $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit $arrayPsRule = $using:arrayPsRule @@ -7209,8 +7210,33 @@ function processDataCollection { #DataCollection Export of All Resources if ($resourcesIdsAll.Count -gt 0) { if (-not $NoCsvExport) { + $startExportingResourcesAllCSV = Get-Date Write-Host "Exporting ResourcesAll CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv'" - $resourcesIdsAll | Sort-Object -Property id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + $arrListSKUKeys = [System.Collections.ArrayList]@() + foreach ($entry in $resourcesIdsAll.where({ $_.sku }).sku) { + foreach ($noteProperty in ($entry | Get-Member).where({ $_.MemberType -eq 'NoteProperty' })) { + if ($arrListSKUKeys -notcontains $noteProperty.Name) { + $null = $arrListSKUKeys.Add($noteProperty.Name) + } + } + } + Write-Host " SKU keys: $(($arrListSKUKeys | Sort-Object) -join ', ')" + + Write-Host ' Enriching resources with SKU keys' + foreach ($entry in $resourcesIdsAll) { + foreach ($key in $arrListSKUKeys | Sort-Object) { + if ($entry.sku.$key) { + $entry | Add-Member -MemberType NoteProperty -Name "sku_$($key)" -Value $entry.sku.$key + } + else { + $entry | Add-Member -MemberType NoteProperty -Name "sku_$($key)" -Value $null + } + } + } + + $resourcesIdsAll | Sort-Object -Property id | Select-Object -Property subscriptionId, subscriptionName, mgPath, type, sku_*, kind, id, name, location, tags, createdTime, changedTime, cafResourceNamingResult, cafResourceNaming, cafResourceNamingFriendlyName | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + $endExportingResourcesAllCSV = Get-Date + Write-Host " Exporting ResourcesAll CSV duration: $((New-TimeSpan -Start $startExportingResourcesAllCSV -End $endExportingResourcesAllCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startExportingResourcesAllCSV -End $endExportingResourcesAllCSV).TotalSeconds) seconds)" } } else { @@ -8940,6 +8966,131 @@ function processManagedIdentities { $endSPMI = Get-Date Write-Host "Processing Service Principals - Managed Identities duration: $((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalSeconds) seconds)" } +function processMDfCCoverage { + Write-Host ' Processing Defender Coverage' + $start = Get-Date + + $htDefenderProps = @{} + $htDefenderExtensions = @{} + foreach ($x in $arrayDefenderPlans) { + if (-not $htDefenderProps.($x.defenderPlan)) { + $htDefenderProps.($x.defenderPlan) = [System.Collections.ArrayList]@() + } + if (-not $htDefenderExtensions.($x.defenderPlan)) { + $htDefenderExtensions.($x.defenderPlan) = [System.Collections.ArrayList]@() + } + foreach ($noteprop in ($x.defenderPlanFull.properties | Get-Member).where({ $_.MemberType -eq 'NoteProperty' })) { + if ($htDefenderProps.($x.defenderPlan) -notcontains $noteprop.Name) { + $null = $htDefenderProps.($x.defenderPlan).Add($noteprop.Name) + } + if ($noteprop.Name -eq 'extensions') { + foreach ($extension in $x.defenderPlanFull.properties.($noteprop.Name)) { + if ($htDefenderExtensions.($x.defenderPlan) -notcontains $extension.name) { + $null = $htDefenderExtensions.($x.defenderPlan).Add($extension.name) + } + } + } + } + } + + $arrayDefenderPlansNamesUnique = $arrayDefenderPlans.defenderPlan | Sort-Object -Unique + $script:arrayDefenderPlansCoverage = [System.Collections.ArrayList]@() + foreach ($defenderPlanName in $arrayDefenderPlansNamesUnique) { + foreach ($defenderPlanEntry in $arrayDefenderPlans.where({ $_.defenderPlan -eq $defenderPlanName })) { + $objDefenderPlan = [ordered]@{ + plan = $defenderPlanEntry.defenderPlan + subscriptionId = $defenderPlanEntry.subscriptionId + subscriptionName = $defenderPlanEntry.subscriptionName + subscriptionMgPath = $defenderPlanEntry.subscriptionMgPath + } + foreach ($prop in $htDefenderProps.($defenderPlanName)) { + if ($prop -eq 'extensions') { + foreach ($extension in $htDefenderExtensions.($defenderPlanName)) { + $extensionObject = $defenderPlanEntry.defenderPlanFull.properties.extensions.where({ $_.name -eq $extension }) + if ($extensionObject.count -gt 0) { + $objDefenderPlan.("ext_$($extension)") = $extensionObject.isEnabled + if ($defenderPlanName -eq 'StorageAccounts' -and $extension -eq 'OnUploadMalwareScanning') { + if ($extensionObject.additionalExtensionProperties.CapGBPerMonthPerStorageAccount) { + $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $extensionObject.additionalExtensionProperties.CapGBPerMonthPerStorageAccount + } + else { + $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $null + } + } + } + else { + $objDefenderPlan.("ext_$($extension)") = $null + if ($defenderPlanName -eq 'StorageAccounts' -and $extension -eq 'OnUploadMalwareScanning') { + $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $null + } + } + } + } + elseif ($prop -eq 'replacedBy') { + $objDefenderPlan.($prop) = $defenderPlanEntry.defenderPlanFull.properties.($prop) -join ';' + } + else { + $objDefenderPlan.($prop) = $defenderPlanEntry.defenderPlanFull.properties.($prop) + } + + if ($defenderPlanName -eq 'VirtualMachines' -and $prop -eq 'subPlan') { + if ($defenderPlanEntry.defenderPlanFull.properties.($prop)) { + if ($htSecuritySettings.($defenderPlanEntry.subscriptionId).WDATP) { + $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = ($htSecuritySettings.($defenderPlanEntry.subscriptionId).WDATP.properties.enabled).ToString() + } + else { + $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = 'unknown' + } + } + else { + $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = 'n/a' + } + + } + } + $null = $script:arrayDefenderPlansCoverage.Add($objDefenderPlan) + } + } + + # $tstsmp = Get-Date -Format 'yyyyMMdd_HHmmss' + # $arrayDefenderPlansCoverage | ConvertTo-Json -Depth 99 > "c:\temp\defenderCoverage_Final_$($tstsmp).json" + + $arrayDefenderPlanSpecificProperties = [System.Collections.ArrayList]@() + $arrayDefenderPlanCommonProperties = @('plan', 'subscriptionId', 'subscriptionName', 'subscriptionMgPath', 'pricingTier', 'freeTrialRemainingTime') + foreach ($plan in $arrayDefenderPlansCoverage) { + $plan.Keys | ForEach-Object { + if ($_ -notin $arrayDefenderPlanCommonProperties) { + $null = $arrayDefenderPlanSpecificProperties.Add("$($plan.plan)_$($_)") + } + } + } + $arrayDefenderPlanSpecificPropertiesUnique = $arrayDefenderPlanSpecificProperties | Sort-Object -Unique + + $arrayDefenderPlansCoverageAll = [System.Collections.ArrayList]@() + foreach ($entry in $arrayDefenderPlansCoverage) { + $obj = [PSCustomObject]@{} + foreach ($cprop in $arrayDefenderPlanCommonProperties) { + $obj | Add-Member -MemberType NoteProperty -Name $cprop -Value $entry.($cprop) + } + foreach ($sprop in $arrayDefenderPlanSpecificPropertiesUnique) { + if ($sprop -like "$($entry.plan)_*") { + $obj | Add-Member -MemberType NoteProperty -Name $sprop -Value $entry.($sprop -replace "$($entry.plan)_", '' ) + } + else { + $obj | Add-Member -MemberType NoteProperty -Name $sprop -Value $null + } + } + $null = $arrayDefenderPlansCoverageAll.Add($obj) + } + + if (-not $NoCsvExport) { + Write-Host " Exporting MDfCCoverage CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCCoverage.csv'" + $arrayDefenderPlansCoverageAll | Sort-Object -Property plan, subscriptionName | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCCoverage.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + + $end = Get-Date + Write-Host " Defender Coverage processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" +} function processNetwork { $start = Get-Date Write-Host "Processing Network enrichment ($($arrayVNets.Count) Virtual Networks)" @@ -22881,6 +23032,118 @@ extensions: [{ name: 'sort' }] Write-Host " Microsoft Defender for Cloud plans by Subscription processing duration: $((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalSeconds) seconds)" #endregion SUMMARYSubDefenderPlansBySubscription + #region SUMMARYSubDefenderCoverage + processMDfCCoverage + #open main div + $htmlTableId = 'TenantSummary_DefenderCoverage' + + #open main div + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + foreach ($mdfcPlanGroup in ($arrayDefenderPlansCoverage | Group-Object -Property { $_.plan })) { + + $tfCount = $mdfcPlanGroup.Group.Count + $htmlTableId = "TenantSummary_DefenderCoverage_$($mdfcPlanGroup.Name)" + $props = $mdfcPlanGroup.Group[0].Keys + $propsCount = $props.Count + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + +$(($props | ForEach-Object { "" }) -join '') + + + +"@) + + foreach ($entry in $mdfcPlanGroup.Group | Sort-Object -Property { $_.subscriptionName }) { + [void]$htmlTenantSummary.AppendLine('') + foreach ($groupKey in $props) { + if ($entry.($groupKey)) { + [void]$htmlTenantSummary.AppendLine("") + } + else { + [void]$htmlTenantSummary.AppendLine('') + } + } + [void]$htmlTenantSummary.AppendLine('') + } + [void]$htmlTenantSummary.AppendLine(@" + +
$_
$($entry.($groupKey))n/a
+
+ +"@) + } + + + #close main div + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion SUMMARYSubDefenderCoverage + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { #region SUMMARYSubUserAssignedIdentities4Resources Write-Host ' processing TenantSummary Subscriptions UserAssigned Managed Identities assigned to Resources' @@ -30573,10 +30836,29 @@ function dataCollectionDefenderPlans { subscriptionMgPath = $childMgMgPath defenderPlan = $defenderPlanResult.name defenderPlanTier = $defenderPlanResult.properties.pricingTier + defenderPlanFull = $defenderPlanResult }) } } } + + $currentTask = "Getting Microsoft Defender for Cloud settings for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/settings?api-version=2022-05-01" + $method = 'GET' + $securitySettingsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if ($securitySettingsResult -eq 'SubScriptionNotRegistered' -or $securitySettingsResult -eq 'DisallowedProvider') { + Write-Host "Subscription $scopeId skipped for SecuritySettings ($securitySettingsResult)" + } + else { + foreach ($setting in $securitySettingsResult) { + if (-not $htSecuritySettings.$scopeId) { + $script:htSecuritySettings.$scopeId = @{} + } + $script:htSecuritySettings.($scopeId).($setting.name) = $setting + } + } + } $funcDataCollectionDefenderPlans = $function:dataCollectionDefenderPlans.ToString() @@ -31029,7 +31311,7 @@ function dataCollectionResources { #region resources LIST $currentTask = "Getting Resources for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resources?`$expand=createdTime,changedTime,properties&api-version=2023-07-01" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resources?`$expand=createdTime,changedTime,properties&api-version=2024-03-01" $method = 'GET' $resourcesSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' #endregion resources LIST @@ -31784,10 +32066,24 @@ function dataCollectionResources { $cafResourceNamingCheck = 'n/a' $applicableNaming = 'n/a' } + + $sku = $null + if ($resource.sku) { + $sku = $resource.sku + } + + $kind = $null + if ($resource.kind) { + $kind = $resource.kind + } + $null = $script:resourcesIdsAll.Add([PSCustomObject]@{ subscriptionId = $scopeId + subscriptionName = $scopeDisplayName mgPath = $childMgMgPath type = ($resource.type).ToLower() + sku = $sku + kind = $kind id = ($resource.Id).ToLower() name = ($resource.name).ToLower() location = ($resource.location).ToLower() @@ -35341,6 +35637,7 @@ if (-not $HierarchyMapOnly) { $htDailySummary = @{} $arrayDefenderPlans = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) $arrayDefenderPlansSubscriptionsSkipped = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $htSecuritySettings = [System.Collections.Hashtable]::Synchronized(@{}) $arrayUserAssignedIdentities4Resources = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) $htSubscriptionsRoleAssignmentLimit = [System.Collections.Hashtable]::Synchronized(@{}) if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index f2ae5fd2..c48a0e79 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -371,7 +371,7 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.5.5', + $ProductVersion = '6.6.0', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -679,6 +679,7 @@ if ($ManagementGroupId -match ' ') { } #region Functions +. ".\$($ScriptPath)\functions\processMDfCCoverage.ps1" . ".\$($ScriptPath)\functions\getPrivateEndpointCapableResourceTypes.ps1" . ".\$($ScriptPath)\functions\validateLeastPrivilegeForUser.ps1" . ".\$($ScriptPath)\functions\getPolicyRemediation.ps1" @@ -1024,6 +1025,7 @@ if (-not $HierarchyMapOnly) { $htDailySummary = @{} $arrayDefenderPlans = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) $arrayDefenderPlansSubscriptionsSkipped = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $htSecuritySettings = [System.Collections.Hashtable]::Synchronized(@{}) $arrayUserAssignedIdentities4Resources = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) $htSubscriptionsRoleAssignmentLimit = [System.Collections.Hashtable]::Synchronized(@{}) if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { diff --git a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 index f54d2dfa..9eafcac0 100644 --- a/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 +++ b/pwsh/dev/functions/dataCollection/dataCollectionFunctions.ps1 @@ -52,10 +52,29 @@ function dataCollectionDefenderPlans { subscriptionMgPath = $childMgMgPath defenderPlan = $defenderPlanResult.name defenderPlanTier = $defenderPlanResult.properties.pricingTier + defenderPlanFull = $defenderPlanResult }) } } } + + $currentTask = "Getting Microsoft Defender for Cloud settings for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/settings?api-version=2022-05-01" + $method = 'GET' + $securitySettingsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if ($securitySettingsResult -eq 'SubScriptionNotRegistered' -or $securitySettingsResult -eq 'DisallowedProvider') { + Write-Host "Subscription $scopeId skipped for SecuritySettings ($securitySettingsResult)" + } + else { + foreach ($setting in $securitySettingsResult) { + if (-not $htSecuritySettings.$scopeId) { + $script:htSecuritySettings.$scopeId = @{} + } + $script:htSecuritySettings.($scopeId).($setting.name) = $setting + } + } + } $funcDataCollectionDefenderPlans = $function:dataCollectionDefenderPlans.ToString() @@ -508,7 +527,7 @@ function dataCollectionResources { #region resources LIST $currentTask = "Getting Resources for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" - $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resources?`$expand=createdTime,changedTime,properties&api-version=2023-07-01" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resources?`$expand=createdTime,changedTime,properties&api-version=2024-03-01" $method = 'GET' $resourcesSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' #endregion resources LIST @@ -1263,10 +1282,24 @@ function dataCollectionResources { $cafResourceNamingCheck = 'n/a' $applicableNaming = 'n/a' } + + $sku = $null + if ($resource.sku) { + $sku = $resource.sku + } + + $kind = $null + if ($resource.kind) { + $kind = $resource.kind + } + $null = $script:resourcesIdsAll.Add([PSCustomObject]@{ subscriptionId = $scopeId + subscriptionName = $scopeDisplayName mgPath = $childMgMgPath type = ($resource.type).ToLower() + sku = $sku + kind = $kind id = ($resource.Id).ToLower() name = ($resource.name).ToLower() location = ($resource.location).ToLower() diff --git a/pwsh/dev/functions/getOrphanedResources.ps1 b/pwsh/dev/functions/getOrphanedResources.ps1 index a932a120..266c2ea4 100644 --- a/pwsh/dev/functions/getOrphanedResources.ps1 +++ b/pwsh/dev/functions/getOrphanedResources.ps1 @@ -279,7 +279,7 @@ resources intent = $intent }) - $intent = 'misconfiguration' + $intent = 'cost savings' $null = $queries.Add([PSCustomObject]@{ queryName = 'microsoft.network/privateendpoints' query = @" diff --git a/pwsh/dev/functions/processDataCollection.ps1 b/pwsh/dev/functions/processDataCollection.ps1 index 35824935..ae1c2500 100644 --- a/pwsh/dev/functions/processDataCollection.ps1 +++ b/pwsh/dev/functions/processDataCollection.ps1 @@ -345,6 +345,7 @@ $htUserTypesGuest = $using:htUserTypesGuest $arrayDefenderPlans = $using:arrayDefenderPlans $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped + $htSecuritySettings = $using:htSecuritySettings $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit $arrayPsRule = $using:arrayPsRule @@ -887,8 +888,33 @@ #DataCollection Export of All Resources if ($resourcesIdsAll.Count -gt 0) { if (-not $NoCsvExport) { + $startExportingResourcesAllCSV = Get-Date Write-Host "Exporting ResourcesAll CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv'" - $resourcesIdsAll | Sort-Object -Property id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + $arrListSKUKeys = [System.Collections.ArrayList]@() + foreach ($entry in $resourcesIdsAll.where({ $_.sku }).sku) { + foreach ($noteProperty in ($entry | Get-Member).where({ $_.MemberType -eq 'NoteProperty' })) { + if ($arrListSKUKeys -notcontains $noteProperty.Name) { + $null = $arrListSKUKeys.Add($noteProperty.Name) + } + } + } + Write-Host " SKU keys: $(($arrListSKUKeys | Sort-Object) -join ', ')" + + Write-Host ' Enriching resources with SKU keys' + foreach ($entry in $resourcesIdsAll) { + foreach ($key in $arrListSKUKeys | Sort-Object) { + if ($entry.sku.$key) { + $entry | Add-Member -MemberType NoteProperty -Name "sku_$($key)" -Value $entry.sku.$key + } + else { + $entry | Add-Member -MemberType NoteProperty -Name "sku_$($key)" -Value $null + } + } + } + + $resourcesIdsAll | Sort-Object -Property id | Select-Object -Property subscriptionId, subscriptionName, mgPath, type, sku_*, kind, id, name, location, tags, createdTime, changedTime, cafResourceNamingResult, cafResourceNaming, cafResourceNamingFriendlyName | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + $endExportingResourcesAllCSV = Get-Date + Write-Host " Exporting ResourcesAll CSV duration: $((New-TimeSpan -Start $startExportingResourcesAllCSV -End $endExportingResourcesAllCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startExportingResourcesAllCSV -End $endExportingResourcesAllCSV).TotalSeconds) seconds)" } } else { diff --git a/pwsh/dev/functions/processMDfCCoverage.ps1 b/pwsh/dev/functions/processMDfCCoverage.ps1 new file mode 100644 index 00000000..c9e4c6ee --- /dev/null +++ b/pwsh/dev/functions/processMDfCCoverage.ps1 @@ -0,0 +1,125 @@ +function processMDfCCoverage { + Write-Host ' Processing Defender Coverage' + $start = Get-Date + + $htDefenderProps = @{} + $htDefenderExtensions = @{} + foreach ($x in $arrayDefenderPlans) { + if (-not $htDefenderProps.($x.defenderPlan)) { + $htDefenderProps.($x.defenderPlan) = [System.Collections.ArrayList]@() + } + if (-not $htDefenderExtensions.($x.defenderPlan)) { + $htDefenderExtensions.($x.defenderPlan) = [System.Collections.ArrayList]@() + } + foreach ($noteprop in ($x.defenderPlanFull.properties | Get-Member).where({ $_.MemberType -eq 'NoteProperty' })) { + if ($htDefenderProps.($x.defenderPlan) -notcontains $noteprop.Name) { + $null = $htDefenderProps.($x.defenderPlan).Add($noteprop.Name) + } + if ($noteprop.Name -eq 'extensions') { + foreach ($extension in $x.defenderPlanFull.properties.($noteprop.Name)) { + if ($htDefenderExtensions.($x.defenderPlan) -notcontains $extension.name) { + $null = $htDefenderExtensions.($x.defenderPlan).Add($extension.name) + } + } + } + } + } + + $arrayDefenderPlansNamesUnique = $arrayDefenderPlans.defenderPlan | Sort-Object -Unique + $script:arrayDefenderPlansCoverage = [System.Collections.ArrayList]@() + foreach ($defenderPlanName in $arrayDefenderPlansNamesUnique) { + foreach ($defenderPlanEntry in $arrayDefenderPlans.where({ $_.defenderPlan -eq $defenderPlanName })) { + $objDefenderPlan = [ordered]@{ + plan = $defenderPlanEntry.defenderPlan + subscriptionId = $defenderPlanEntry.subscriptionId + subscriptionName = $defenderPlanEntry.subscriptionName + subscriptionMgPath = $defenderPlanEntry.subscriptionMgPath + } + foreach ($prop in $htDefenderProps.($defenderPlanName)) { + if ($prop -eq 'extensions') { + foreach ($extension in $htDefenderExtensions.($defenderPlanName)) { + $extensionObject = $defenderPlanEntry.defenderPlanFull.properties.extensions.where({ $_.name -eq $extension }) + if ($extensionObject.count -gt 0) { + $objDefenderPlan.("ext_$($extension)") = $extensionObject.isEnabled + if ($defenderPlanName -eq 'StorageAccounts' -and $extension -eq 'OnUploadMalwareScanning') { + if ($extensionObject.additionalExtensionProperties.CapGBPerMonthPerStorageAccount) { + $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $extensionObject.additionalExtensionProperties.CapGBPerMonthPerStorageAccount + } + else { + $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $null + } + } + } + else { + $objDefenderPlan.("ext_$($extension)") = $null + if ($defenderPlanName -eq 'StorageAccounts' -and $extension -eq 'OnUploadMalwareScanning') { + $objDefenderPlan.("ext_$("$($extension)_CapGBPerMonthPerStorageAccount")") = $null + } + } + } + } + elseif ($prop -eq 'replacedBy') { + $objDefenderPlan.($prop) = $defenderPlanEntry.defenderPlanFull.properties.($prop) -join ';' + } + else { + $objDefenderPlan.($prop) = $defenderPlanEntry.defenderPlanFull.properties.($prop) + } + + if ($defenderPlanName -eq 'VirtualMachines' -and $prop -eq 'subPlan') { + if ($defenderPlanEntry.defenderPlanFull.properties.($prop)) { + if ($htSecuritySettings.($defenderPlanEntry.subscriptionId).WDATP) { + $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = ($htSecuritySettings.($defenderPlanEntry.subscriptionId).WDATP.properties.enabled).ToString() + } + else { + $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = 'unknown' + } + } + else { + $objDefenderPlan.('ext_MicrosoftDefenderforEndpoint') = 'n/a' + } + + } + } + $null = $script:arrayDefenderPlansCoverage.Add($objDefenderPlan) + } + } + + # $tstsmp = Get-Date -Format 'yyyyMMdd_HHmmss' + # $arrayDefenderPlansCoverage | ConvertTo-Json -Depth 99 > "c:\temp\defenderCoverage_Final_$($tstsmp).json" + + $arrayDefenderPlanSpecificProperties = [System.Collections.ArrayList]@() + $arrayDefenderPlanCommonProperties = @('plan', 'subscriptionId', 'subscriptionName', 'subscriptionMgPath', 'pricingTier', 'freeTrialRemainingTime') + foreach ($plan in $arrayDefenderPlansCoverage) { + $plan.Keys | ForEach-Object { + if ($_ -notin $arrayDefenderPlanCommonProperties) { + $null = $arrayDefenderPlanSpecificProperties.Add("$($plan.plan)_$($_)") + } + } + } + $arrayDefenderPlanSpecificPropertiesUnique = $arrayDefenderPlanSpecificProperties | Sort-Object -Unique + + $arrayDefenderPlansCoverageAll = [System.Collections.ArrayList]@() + foreach ($entry in $arrayDefenderPlansCoverage) { + $obj = [PSCustomObject]@{} + foreach ($cprop in $arrayDefenderPlanCommonProperties) { + $obj | Add-Member -MemberType NoteProperty -Name $cprop -Value $entry.($cprop) + } + foreach ($sprop in $arrayDefenderPlanSpecificPropertiesUnique) { + if ($sprop -like "$($entry.plan)_*") { + $obj | Add-Member -MemberType NoteProperty -Name $sprop -Value $entry.($sprop -replace "$($entry.plan)_", '' ) + } + else { + $obj | Add-Member -MemberType NoteProperty -Name $sprop -Value $null + } + } + $null = $arrayDefenderPlansCoverageAll.Add($obj) + } + + if (-not $NoCsvExport) { + Write-Host " Exporting MDfCCoverage CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCCoverage.csv'" + $arrayDefenderPlansCoverageAll | Sort-Object -Property plan, subscriptionName | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCCoverage.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + + $end = Get-Date + Write-Host " Defender Coverage processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((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 5f34deb4..830878f4 100644 --- a/pwsh/dev/functions/processTenantSummary.ps1 +++ b/pwsh/dev/functions/processTenantSummary.ps1 @@ -8885,6 +8885,118 @@ extensions: [{ name: 'sort' }] Write-Host " Microsoft Defender for Cloud plans by Subscription processing duration: $((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalSeconds) seconds)" #endregion SUMMARYSubDefenderPlansBySubscription + #region SUMMARYSubDefenderCoverage + processMDfCCoverage + #open main div + $htmlTableId = 'TenantSummary_DefenderCoverage' + + #open main div + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + foreach ($mdfcPlanGroup in ($arrayDefenderPlansCoverage | Group-Object -Property { $_.plan })) { + + $tfCount = $mdfcPlanGroup.Group.Count + $htmlTableId = "TenantSummary_DefenderCoverage_$($mdfcPlanGroup.Name)" + $props = $mdfcPlanGroup.Group[0].Keys + $propsCount = $props.Count + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + +$(($props | ForEach-Object { "" }) -join '') + + + +"@) + + foreach ($entry in $mdfcPlanGroup.Group | Sort-Object -Property { $_.subscriptionName }) { + [void]$htmlTenantSummary.AppendLine('') + foreach ($groupKey in $props) { + if ($entry.($groupKey)) { + [void]$htmlTenantSummary.AppendLine("") + } + else { + [void]$htmlTenantSummary.AppendLine('') + } + } + [void]$htmlTenantSummary.AppendLine('') + } + [void]$htmlTenantSummary.AppendLine(@" + +
$_
$($entry.($groupKey))n/a
+
+ +"@) + } + + + #close main div + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion SUMMARYSubDefenderCoverage + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { #region SUMMARYSubUserAssignedIdentities4Resources Write-Host ' processing TenantSummary Subscriptions UserAssigned Managed Identities assigned to Resources' diff --git a/version.json b/version.json index e7ded1ef..10366bbd 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "ProductVersion": "6.5.5" + "ProductVersion": "6.6.0" } \ No newline at end of file