Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/powershell/tests/Test-Assessment.61004.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Defender CSPM is the plan in Microsoft Defender for Cloud that scans your subscriptions, inventories your AI workloads, and surfaces risks through security recommendations and attack path analysis. Without it, there is no AI inventory, no flagging of misconfigured AI services, and no modeling of how an attacker could reach sensitive grounding data through an exposed endpoint. This check verifies the plan is enabled on every subscription that hosts AI workloads.

When Defender CSPM is not enabled on a subscription hosting AI workloads, misconfigured AI service accounts — publicly exposed, secured only by API keys, lacking private endpoints — are never surfaced as risks and remain accessible indefinitely. Threat actors can exploit this visibility gap because the attack path from an exposed AI endpoint to the sensitive grounding or fine-tuning data it connects to has never been modeled by defenders. Without Defender CSPM, the organization cannot identify those paths before an attacker follows them.

**Source:** [Overview - AI security posture management](https://learn.microsoft.com/azure/defender-for-cloud/ai-security-posture)

**Remediation action**

- [Enable Microsoft Defender for Cloud CSPM plan](https://learn.microsoft.com/azure/defender-for-cloud/tutorial-enable-cspm-plan)
- [AI security posture management in Defender for Cloud](https://learn.microsoft.com/azure/defender-for-cloud/ai-security-posture)
- [Enable plans programmatically (PUT .../pricings/CloudPosture)](https://learn.microsoft.com/rest/api/defenderforcloud/pricings/update)

<!--- Results --->
%TestResult%
163 changes: 163 additions & 0 deletions src/powershell/tests/Test-Assessment.61004.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<#
.SYNOPSIS
Validates that Microsoft Defender for Cloud CSPM plan is enabled on all Azure subscriptions.

.DESCRIPTION
This test evaluates the Defender for Cloud CloudPosture pricing tier for every enabled
Azure subscription. A subscription passes only when pricingTier is 'Standard'.
Without the CSPM plan active, AI posture recommendations, attack-path visibility, and
AI Security Posture features cannot run on the subscription.

.NOTES
Test ID: 61004
Category: AI Cloud Posture
Required APIs: Azure Resource Graph (resourcecontainers/subscriptions),
Azure Management REST API (Microsoft.Security/pricings/CloudPosture)
#>

function Test-Assessment-61004 {

[ZtTest(
Category = 'AI Cloud Posture',
Comment thread
praneeth-0000 marked this conversation as resolved.
ImplementationCost = 'Medium',
Service = ('Azure'),
MinimumLicense = ('Microsoft_Defender_for_Cloud'),
Comment thread
praneeth-0000 marked this conversation as resolved.
Pillar = 'AI',
RiskLevel = 'High',
SfiPillar = 'Protect tenants and production systems',
TenantType = ('Workforce'),
TestId = 61004,
Title = 'Microsoft Defender for Cloud CSPM plan is enabled on all Azure subscriptions',
UserImpact = 'Low'
)]
[CmdletBinding()]
param()

#region Data Collection

Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
$activity = 'Evaluating Microsoft Defender for Cloud CSPM plan configuration'

# Q1: Enumerate all enabled subscriptions via Azure Resource Graph
Write-ZtProgress -Activity $activity -Status 'Querying enabled subscriptions via Resource Graph'

$argQuery = @"
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions'
| where properties.state =~ 'Enabled'
| project subscriptionId, displayName=name
"@

$allSubscriptions = @()
try {
$allSubscriptions = @(Invoke-ZtAzureResourceGraphRequest -Query $argQuery)
Write-PSFMessage "ARG Query returned $($allSubscriptions.Count) enabled subscription(s)" -Tag Test -Level VeryVerbose
}
catch {
Write-PSFMessage "Azure Resource Graph query failed: $($_.Exception.Message)" -Tag Test -Level Warning
Add-ZtTestResultDetail -SkippedBecause NotSupported
return
}

if ($allSubscriptions.Count -eq 0) {
Write-PSFMessage 'No enabled Azure subscriptions found.' -Tag Test -Level VeryVerbose
Add-ZtTestResultDetail -SkippedBecause NotApplicable
return
}

# Q2: For each subscription, read the Defender for Cloud CloudPosture pricing plan
Write-ZtProgress -Activity $activity -Status 'Querying Defender for Cloud CSPM plan per subscription'

$evaluationResults = @()

foreach ($subscription in $allSubscriptions) {
$subscriptionId = $subscription.subscriptionId
$displayName = $subscription.displayName

$pricingPath = "/subscriptions/$subscriptionId/providers/Microsoft.Security/pricings/CloudPosture?api-version=2024-01-01"
$pricingTier = 'Not Found'

try {
$pricingResponse = Invoke-ZtAzureRequest -Path $pricingPath
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every Q2 query treats an exception as PricingTier = 'Not Found', including 403/Forbidden. The spec says “not authorised” must be surfaced as an assessment-framework permission issue, not a fail. This will produce false failures when the runner can enumerate subscriptions but lacks Security Reader on one subscription.

$pricingTier = if ($pricingResponse.properties.pricingTier) { $pricingResponse.properties.pricingTier } else { 'Unknown' }
}
catch {
Write-PSFMessage "Error querying Defender CSPM plan for subscription '$displayName' ($subscriptionId): $_" -Tag Test -Level Warning
}

$evaluationResults += [PSCustomObject]@{
SubscriptionId = $subscriptionId
DisplayName = $displayName
PricingTier = $pricingTier
}
}

#endregion Data Collection

#region Assessment Logic

$failedItems = @($evaluationResults | Where-Object { $_.PricingTier -ne 'Standard' })
$passed = $failedItems.Count -eq 0

if ($passed) {
$testResultMarkdown = "✅ Microsoft Defender for Cloud CSPM plan (Standard tier) is enabled on every in-scope Azure subscription.`n`n%TestResult%"
}
else {
$testResultMarkdown = "❌ One or more in-scope Azure subscriptions do not have the Defender CSPM plan enabled.`n`n%TestResult%"
}

#endregion Assessment Logic

#region Report Generation

$portalCspmLink = 'https://portal.azure.com/#view/Microsoft_Azure_Security/SecurityMenuBlade/~/EnvironmentSettings'
$portalSubPricingBaseLink = 'https://portal.azure.com/#view/Microsoft_Azure_Security/PolicyMenuBlade/~/pricingTier/subscriptionId'

$formatTemplate = @'


## [{0}]({1})

| Display name | Pricing tier |
| :----------- | :----------- |
{2}
'@

$mdInfo = ''
if ($failedItems.Count -gt 0) {
$tableRows = ''
$maxItemsToDisplay = 10
$displayResults = @($failedItems | Sort-Object DisplayName)
$hasMoreItems = $false
if ($displayResults.Count -gt $maxItemsToDisplay) {
$displayResults = @($displayResults | Select-Object -First $maxItemsToDisplay)
$hasMoreItems = $true
}

foreach ($result in $displayResults) {
$displayNameLink = "[$(Get-SafeMarkdown $result.DisplayName)]($portalSubPricingBaseLink/$($result.SubscriptionId))"
$pricingTierSafe = $result.PricingTier
$tableRows += "| $displayNameLink | $pricingTierSafe |`n"
}

if ($hasMoreItems) {
$remainingCount = $failedItems.Count - $maxItemsToDisplay
$tableRows += "`n... and $remainingCount more. [View all subscriptions in Microsoft Defender for Cloud]($portalCspmLink)`n"
}

$mdInfo = $formatTemplate -f 'Subscriptions missing Defender CSPM plan', $portalCspmLink, $tableRows
}

$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo

#endregion Report Generation

$params = @{
TestId = '61004'
Title = 'Microsoft Defender for Cloud CSPM plan is enabled on all Azure subscriptions'
Status = $passed
Result = $testResultMarkdown
}

Add-ZtTestResultDetail @params
}
Loading