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("$($entry.($groupKey)) | ")
+ }
+ else {
+ [void]$htmlTenantSummary.AppendLine('n/a | ')
+ }
+ }
+ [void]$htmlTenantSummary.AppendLine('
')
+ }
+ [void]$htmlTenantSummary.AppendLine(@"
+
+
+
+
+"@)
+ }
+
+
+ #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("$($entry.($groupKey)) | ")
+ }
+ else {
+ [void]$htmlTenantSummary.AppendLine('n/a | ')
+ }
+ }
+ [void]$htmlTenantSummary.AppendLine('
')
+ }
+ [void]$htmlTenantSummary.AppendLine(@"
+
+
+
+
+"@)
+ }
+
+
+ #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