diff --git a/Powershell scripts/Defender for Servers on resource level/ResourceLevelPricingAtScale.ps1 b/Powershell scripts/Defender for Servers on resource level/ResourceLevelPricingAtScale.ps1 index e7677ce04..635b7b80d 100644 --- a/Powershell scripts/Defender for Servers on resource level/ResourceLevelPricingAtScale.ps1 +++ b/Powershell scripts/Defender for Servers on resource level/ResourceLevelPricingAtScale.ps1 @@ -9,6 +9,48 @@ $arcCount = 0 $vmResponseMachines = $null $vmssResponseMachines = $null $arcResponseMachines = $null +$outTable = @() + +# Function to extract the desired part +function ExtractResourceGroupAndMachine { + param ( + [string]$inputString + ) + # Split the input string by '/' + $parts = $inputString -split '/' + + # Find the indices of the desired parts + $resourceGroupIndex = 4 + $machineIndex = 8 + + try{ + return "$($parts[$resourceGroupIndex])/$($parts[$machineIndex])" + } + catch{ + return $null + } +} + +# Function to add a row to the table +function AddRowToOutTable { + param ( + [string]$simplifiedResourceId, + [string]$resourceType, + [string]$inherited, + [string]$subPlan, + [string]$pricingTier + ) + $row = [PSCustomObject]@{ + ResourceId = $simplifiedResourceId + ResourceType = $resourceType + Inherited = $inherited + SubPlan = $subPlan + PricingTier = $pricingTier + } + #Write-Host "Adding row: $($simplifiedResourceId), $($resourceType), $($inherited), $($subPlan), $($pricingTier)" -ForegroundColor Gray + $script:outTable += $row +} + # login: $needLogin = $true @@ -44,9 +86,9 @@ $expireson = Get-AzAccessToken | Select-Object -ExpandProperty expireson | Selec # Define variables for authentication and resource group $SubscriptionId = Read-Host "Enter your SubscriptionId" -$mode = Read-Host "Enter 'RG' to set pricing for all resourced under a given Resource Group, or 'TAG' to set pricing for all resources with a given tagName and tagValue" -while($mode.ToLower() -ne "rg" -and $mode.ToLower() -ne "tag"){ - $mode = Read-Host "Enter 'RG' to set pricing for all resources under a given Resource Group, or 'TAG' to set pricing for all resources with a given tagName and tagValue" +$mode = Read-Host "Enter 'RG' to read/set pricing for all resourced under a given Resource Group, or 'TAG' to read/set pricing for all resources with a given tagName and tagValue or 'ALL' to read/set pricing for all resources in the subscription" +while($mode.ToLower() -ne "rg" -and $mode.ToLower() -ne "tag" -and $mode.ToLower() -ne "all"){ + $mode = Read-Host "Enter 'RG' to read/set pricing for all resourced under a given Resource Group, or 'TAG' to read/set pricing for all resources with a given tagName and tagValue or 'ALL' to read/set pricing for all resources in the subscription" } if ($mode.ToLower() -eq "rg") { @@ -120,6 +162,38 @@ if ($mode.ToLower() -eq "rg") { Write-Host "Response StatusDescription:" $_.Exception.Response.StatusDescription -ForegroundColor Red Write-Host "Error from response:" $_.ErrorDetails -ForegroundColor Red } +} elseif ($mode.ToLower() -eq "all") { + try + { + # Get all virtual machines, VMSSs, and ARC machines in the resource group based on the given tag + $vmUrl = "https://management.azure.com/subscriptions/" + $SubscriptionId + "/resources?`$filter=resourceType eq 'Microsoft.Compute/virtualMachines'&api-version=2021-04-01" + do{ + $vmResponse = Invoke-RestMethod -Method Get -Uri $vmUrl -Headers @{Authorization = "Bearer $accessToken"} + $vmResponseMachines += $vmResponse.value + $vmUrl = $vmResponse.nextLink + } while (![string]::IsNullOrEmpty($vmUrl)) + + $vmssUrl = "https://management.azure.com/subscriptions/" + $SubscriptionId + "/resources?`$filter=resourceType eq 'Microsoft.Compute/virtualMachineScaleSets'&api-version=2021-04-01" + do{ + $vmssResponse += Invoke-RestMethod -Method Get -Uri $vmssUrl -Headers @{Authorization = "Bearer $accessToken"} + $vmssResponseMachines = $vmssResponse.value + $vmssUrl = $vmssResponse.nextLink + } while (![string]::IsNullOrEmpty($vmssUrl)) + + $arcUrl = "https://management.azure.com/subscriptions/" + $SubscriptionId + "/resources?`$filter=resourceType eq 'Microsoft.HybridCompute/machines'&api-version=2023-07-01" + do{ + $arcResponse += Invoke-RestMethod -Method Get -Uri $arcUrl -Headers @{Authorization = "Bearer $accessToken"} + $arcResponseMachines = $arcResponse.value + $arcUrl = $arcResponse.nextLink + } while (![string]::IsNullOrEmpty($arcUrl)) + } + catch + { + Write-Host "Failed to Get resources! " -ForegroundColor Red + Write-Host "Response StatusCode:" $_.Exception.Response.StatusCode.value__ -ForegroundColor Red + Write-Host "Response StatusDescription:" $_.Exception.Response.StatusDescription -ForegroundColor Red + Write-Host "Error from response:" $_.ErrorDetails -ForegroundColor Red + } } else { Write-Host "Entered invalid mode. Exiting script." exit 1; @@ -159,9 +233,64 @@ if ($continue.ToLower() -eq "n") { } Write-Host "-------------------" -$PricingTier = Read-Host "Enter the command set these resources - 'Free' or 'Standard' or 'Delete' or 'Read' (choosing 'Free' will remove the Defender protection; 'Standard' will enable the 'P1' subplan; 'Delete' will remove any explicitly set configuration (the resource will inherit the parent's configuration); 'Read' will read the current configuration)" -while($PricingTier.ToLower() -ne "free" -and $PricingTier.ToLower() -ne "standard" -and $PricingTier.ToLower() -ne "delete" -and $PricingTier.ToLower() -ne "read"){ -$PricingTier = Read-Host "Enter the command for these resources - 'Free' or 'Standard' or 'Delete' or 'Read' (choosing 'Free' will remove the Defender protection; 'Standard' will enable the 'P1' subplan; 'Delete' will remove any explicitly set configuration (the resource will inherit the parent's configuration); 'Read' will read the current configuration)" +$InputCommand = Read-Host "Enter the command set these resources - 'Free' or 'Standard' or 'Revert' or 'Read' (choosing 'Free' will remove the Defender protection; 'Standard' will enable the 'P1' subplan; 'Revert' will remove any explicitly set configuration (the resource will inherit the parent's configuration); 'Read' will read the current configuration)" +while($InputCommand.ToLower() -ne "free" -and $InputCommand.ToLower() -ne "standard" -and $InputCommand.ToLower() -ne "revert" -and $InputCommand.ToLower() -ne "read"){ + $InputCommand = Read-Host "Enter the command for these resources - 'Free' or 'Standard' or 'Revert' or 'Read' (choosing 'Free' will remove the Defender protection; 'Standard' will enable the 'P1' subplan; 'Revert' will remove any explicitly set configuration (the resource will inherit the parent's configuration); 'Read' will read the current configuration)" +} + +$saveToCsvPath = $null +$readCsvPath = $True + +if ($InputCommand.ToLower() -eq "read") { + while ($readCsvPath) { + $saveToCsvPath = Read-Host "Path of the new or existing CSV file where the configuration should be exported. Leave blank if no export is required" + + if (-not [string]::IsNullOrEmpty($saveToCsvPath)) { + if (Test-Path $saveToCsvPath) { + # Path does exists + if ((Get-Item $saveToCsvPath).PSIsContainer) { + # Path is a folder + Write-Host "Wrong path: please specify a path of a new or existing CSV file in an existing folder" + $saveToCsvPath = $null + } else { + # Path is a file + if ($saveToCsvPath -like "*.csv") { + # File is a CSV + $overwrite = Read-Host "The CSV file already exists. Do you want to overwrite it? (yes/no)" + if ($overwrite.ToLower() -eq "yes") { + $readCsvPath = $False + } + else { + $saveToCsvPath = $null + } + } else { + # File is not a CSV + Write-Host "Wrong path: the specified file is not a CSV. Please enter the path of a new or existing CSV file" + $saveToCsvPath = $null + } + } + } else { + # Path does not exist + if ((Get-Item (Split-Path $saveToCsvPath -Parent) -ErrorAction SilentlyContinue).PSIsContainer) { + # Parent folder exists + if ($saveToCsvPath -notlike "*.csv") { + Write-Host "Wrong path: the specified file is not a CSV file. Please enter the path of a new or existing CSV file" + $saveToCsvPath = $null + } + else{ + $readCsvPath = $False + } + } else { + # Parent folder does not exist + Write-Host "Wrong path: the specified path is in a non-existing folder. Please enter the path of a new or existing CSV file in an existing folder" + $saveToCsvPath = $null + } + } + } + else{ + $readCsvPath = $False + } + } } # Loop through each machine and update the pricing configuration @@ -183,37 +312,49 @@ foreach ($machine in $vmResponseMachines) { } $pricingUrl = "https://management.azure.com$($machine.id)/providers/Microsoft.Security/pricings/virtualMachines?api-version=2024-01-01" - if($PricingTier.ToLower() -eq "free") + if($InputCommand.ToLower() -eq "free") { $pricingBody = @{ "properties" = @{ - "pricingTier" = $PricingTier + "pricingTier" = "Free" } } - } else + } elseif($InputCommand.ToLower() -eq "standard") { - $subplan = "P1" $pricingBody = @{ "properties" = @{ - "pricingTier" = $PricingTier - "subPlan" = $subplan + "pricingTier" = "Standard" + "subPlan" = "P1" } } } Write-Host "Processing (setting or reading) pricing configuration for '$($machine.name)':" try { - if($PricingTier.ToLower() -eq "delete") + if($InputCommand.ToLower() -eq "revert") { $pricingResponse = Invoke-RestMethod -Method Delete -Uri $pricingUrl -Headers @{Authorization = "Bearer $accessToken"} -ContentType "application/json" -TimeoutSec 120 - Write-Host "Successfully deleted pricing configuration for $($machine.name)" -ForegroundColor Green + Write-Host "Successfully reverted pricing configuration for $($machine.name)" -ForegroundColor Green $successCount++ $vmSuccessCount++ - } elseif ($PricingTier.ToLower() -eq "read") + } elseif ($InputCommand.ToLower() -eq "read") { $pricingResponse = Invoke-RestMethod -Method Get -Uri $pricingUrl -Headers @{Authorization = "Bearer $accessToken"} -ContentType "application/json" -TimeoutSec 120 Write-Host "Successfully read pricing configuration for $($machine.name): " -ForegroundColor Green Write-Host ($pricingResponse | ConvertTo-Json -Depth 100) + try{ + $id = ExtractResourceGroupAndMachine -inputString $pricingResponse.id + $name = $pricingResponse.name + $inherited = $pricingResponse.properties.inherited + $subPlan = $pricingResponse.properties.subPlan + $pricingTier = $pricingResponse.properties.pricingTier + AddRowToOutTable -simplifiedResourceId $id -resourceType "VM" -inherited $inherited -subPlan $subPlan -pricingTier $pricingTier + } + catch{ + Write-Host "Could not write a row in the results table for $($machine.name)" -ForegroundColor Yellow + Write-Host "Exception caught: $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host "Full Exception: $($_.Exception | Format-List -Force)" -ForegroundColor Yellow + } $successCount++ $vmSuccessCount++ } @@ -227,7 +368,7 @@ foreach ($machine in $vmResponseMachines) { } catch { $failureCount++ - Write-Host "Failed to update pricing configuration for $($machine.name)" -ForegroundColor Red + Write-Host "Failed to process (set or read) pricing configuration for $($machine.name)" -ForegroundColor Red Write-Host "Response StatusCode:" $_.Exception.Response.StatusCode.value__ -ForegroundColor Red Write-Host "Response StatusDescription:" $_.Exception.Response.StatusDescription -ForegroundColor Red Write-Host "Error from response:" $_.ErrorDetails -ForegroundColor Red @@ -253,20 +394,19 @@ foreach ($machine in $vmssResponseMachines) { } $pricingUrl = "https://management.azure.com$($machine.id)/providers/Microsoft.Security/pricings/virtualMachines?api-version=2024-01-01" - if($PricingTier.ToLower() -eq "free") + if($InputCommand.ToLower() -eq "free") { $pricingBody = @{ "properties" = @{ - "pricingTier" = $PricingTier + "pricingTier" = "Free" } } - } else + } elseif($InputCommand.ToLower() -eq "standard") { - $subplan = "P1" $pricingBody = @{ "properties" = @{ - "pricingTier" = $PricingTier - "subPlan" = $subplan + "pricingTier" = "Standard" + "subPlan" = "P1" } } } @@ -274,17 +414,30 @@ foreach ($machine in $vmssResponseMachines) { try { - if($PricingTier.ToLower() -eq "delete") + if($InputCommand.ToLower() -eq "revert") { $pricingResponse = Invoke-RestMethod -Method Delete -Uri $pricingUrl -Headers @{Authorization = "Bearer $accessToken"} -ContentType "application/json" -TimeoutSec 120 - Write-Host "Successfully deleted pricing configuration for $($machine.name)" -ForegroundColor Green + Write-Host "Successfully reverted pricing configuration for $($machine.name)" -ForegroundColor Green $successCount++ $vmssSuccessCount++ - } elseif ($PricingTier.ToLower() -eq "read") + } elseif ($InputCommand.ToLower() -eq "read") { $pricingResponse = Invoke-RestMethod -Method Get -Uri $pricingUrl -Headers @{Authorization = "Bearer $accessToken"} -ContentType "application/json" -TimeoutSec 120 Write-Host "Successfully read pricing configuration for $($machine.name): " -ForegroundColor Green Write-Host ($pricingResponse | ConvertTo-Json -Depth 100) + try{ + $id = ExtractResourceGroupAndMachine -inputString $pricingResponse.id + $name = $pricingResponse.name + $inherited = $pricingResponse.properties.inherited + $subPlan = $pricingResponse.properties.subPlan + $pricingTier = $pricingResponse.properties.pricingTier + AddRowToOutTable -simplifiedResourceId $id -resourceType "VMSS Machine" -inherited $inherited -subPlan $subPlan -pricingTier $pricingTier + } + catch{ + Write-Host "Could not write a row in the results table for $($machine.name)" -ForegroundColor Yellow + Write-Host "Exception caught: $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host "Full Exception: $($_.Exception | Format-List -Force)" -ForegroundColor Yellow + } $successCount++ $vmssSuccessCount++ } @@ -298,7 +451,7 @@ foreach ($machine in $vmssResponseMachines) { } catch { $failureCount++ - Write-Host "Failed to update pricing configuration for $($machine.name)" -ForegroundColor Red + Write-Host "Failed to process (set or read) pricing configuration for $($machine.name)" -ForegroundColor Red Write-Host "Response StatusCode:" $_.Exception.Response.StatusCode.value__ -ForegroundColor Red Write-Host "Response StatusDescription:" $_.Exception.Response.StatusDescription -ForegroundColor Red Write-Host "Error from response:" $_.ErrorDetails -ForegroundColor Red @@ -324,20 +477,19 @@ foreach ($machine in $arcResponseMachines) { } $pricingUrl = "https://management.azure.com$($machine.id)/providers/Microsoft.Security/pricings/virtualMachines?api-version=2024-01-01" - if($PricingTier.ToLower() -eq "free") + if($InputCommand.ToLower() -eq "free") { $pricingBody = @{ "properties" = @{ - "pricingTier" = $PricingTier + "pricingTier" = "Free" } } - } else + } elseif($InputCommand.ToLower() -eq "standard") { - $subplan = "P1" $pricingBody = @{ "properties" = @{ - "pricingTier" = $PricingTier - "subPlan" = $subplan + "pricingTier" = "Standard" + "subPlan" = "P1" } } } @@ -345,17 +497,30 @@ foreach ($machine in $arcResponseMachines) { try { - if($PricingTier.ToLower() -eq "delete") + if($InputCommand.ToLower() -eq "revert") { $pricingResponse = Invoke-RestMethod -Method Delete -Uri $pricingUrl -Headers @{Authorization = "Bearer $accessToken"} -ContentType "application/json" -TimeoutSec 120 - Write-Host "Successfully deleted pricing configuration for $($machine.name)" -ForegroundColor Green + Write-Host "Successfully reverted pricing configuration for $($machine.name)" -ForegroundColor Green $successCount++ $arcSuccessCount++ - } elseif ($PricingTier.ToLower() -eq "read") + } elseif ($InputCommand.ToLower() -eq "read") { $pricingResponse = Invoke-RestMethod -Method Get -Uri $pricingUrl -Headers @{Authorization = "Bearer $accessToken"} -ContentType "application/json" -TimeoutSec 120 Write-Host "Successfully read pricing configuration for $($machine.name): " -ForegroundColor Green Write-Host ($pricingResponse | ConvertTo-Json -Depth 100) + try{ + $id = ExtractResourceGroupAndMachine -inputString $pricingResponse.id + $name = $pricingResponse.name + $inherited = $pricingResponse.properties.inherited + $subPlan = $pricingResponse.properties.subPlan + $pricingTier = $pricingResponse.properties.pricingTier + AddRowToOutTable -simplifiedResourceId $id -resourceType "Arc Machine" -inherited $inherited -subPlan $subPlan -pricingTier $pricingTier + } + catch{ + Write-Host "Could not write a row in the results table for $($machine.name)" -ForegroundColor Yellow + Write-Host "Exception caught: $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host "Full Exception: $($_.Exception | Format-List -Force)" -ForegroundColor Yellow + } $successCount++ $arcSuccessCount++ } @@ -369,7 +534,7 @@ foreach ($machine in $arcResponseMachines) { } catch { $failureCount++ - Write-Host "Failed to update pricing configuration for $($machine.name)" -ForegroundColor Red + Write-Host "Failed to process (set or read) pricing configuration for $($machine.name)" -ForegroundColor Red Write-Host "Response StatusCode:" $_.Exception.Response.StatusCode.value__ -ForegroundColor Red Write-Host "Response StatusDescription:" $_.Exception.Response.StatusDescription -ForegroundColor Red Write-Host "Error from response:" $_.ErrorDetails -ForegroundColor Red @@ -401,3 +566,12 @@ Write-Host "-------------------" Write-Host "Overall" Write-Host "Successfully processed (set or read) resources: $successCount" -ForegroundColor Green Write-Host "Failures processing (setting or reading) resources: $failureCount" -ForegroundColor $(if ($failureCount -gt 0) {'Red'} else {'Green'}) + +if($InputCommand.ToLower() -eq "read"){ + $outTable | Format-Table -AutoSize + + if(-not [string]::IsNullOrEmpty($saveToCsvPath)){ + $outTable | Export-Csv -Path $saveToCsvPath -NoTypeInformation + Write-Host "CSV file ready:" $saveToCsvPath -ForegroundColor Green + } +} diff --git a/Powershell scripts/Defender for Servers on resource level/img/Flow.png b/Powershell scripts/Defender for Servers on resource level/img/Flow.png new file mode 100644 index 000000000..05f531e13 Binary files /dev/null and b/Powershell scripts/Defender for Servers on resource level/img/Flow.png differ diff --git a/Powershell scripts/Defender for Servers on resource level/readme.md b/Powershell scripts/Defender for Servers on resource level/readme.md index 0e777b01c..231d70f0c 100644 --- a/Powershell scripts/Defender for Servers on resource level/readme.md +++ b/Powershell scripts/Defender for Servers on resource level/readme.md @@ -4,4 +4,10 @@ By default, Defender for Servers is enabled as a subscription-wide setting, cove This folder contains a [PowerShell script](https://github.com/Azure/Microsoft-Defender-for-Cloud/blob/main/Powershell%20scripts/Defender%20for%20Servers%20on%20resource%20level/ResourceLevelPricingAtScale.ps1) that allows you to select machines based on Azure resource tags, or a resource group to configure them individually rather than using the same plan setting for all machines in a subscription. -To learn more about how to enable Defender for Servers, please read [this documentation](https://learn.microsoft.com/en-us/azure/defender-for-cloud/tutorial-enable-servers-plan). \ No newline at end of file +The latest version of the script also allows actions on ALL the resources of the specified subscription and exports the status of these resources to a CSV file. + +This is the flowchart of the script execution: + +![flowchart](./img/Flow.png) + +To learn more about how to enable Defender for Servers, please read [this documentation](https://learn.microsoft.com/en-us/azure/defender-for-cloud/tutorial-enable-servers-plan).