|
| 1 | +<# |
| 2 | +.SYNOPSIS |
| 3 | + Enterprise generative AI applications are protected from prompt injection attacks through AI Gateway. |
| 4 | +.DESCRIPTION |
| 5 | + Verifies that Prompt Shield (AI Gateway) is properly configured to protect against prompt injection attacks. |
| 6 | + The test passes if prompt policies exist and are enforced either through the Baseline Profile or through |
| 7 | + Security Profiles assigned to Conditional Access policies. |
| 8 | +#> |
| 9 | + |
| 10 | +function Test-Assessment-25415 { |
| 11 | + [ZtTest( |
| 12 | + Category = 'Global Secure Access', |
| 13 | + ImplementationCost = 'Medium', |
| 14 | + MinimumLicense = ('Entra_Premium_Internet_Access'), |
| 15 | + Pillar = 'Network', |
| 16 | + RiskLevel = 'High', |
| 17 | + SfiPillar = 'Protect networks', |
| 18 | + TenantType = ('Workforce'), |
| 19 | + TestId = 25415, |
| 20 | + Title = 'Enterprise generative AI applications are protected from prompt injection attacks through AI Gateway', |
| 21 | + UserImpact = 'Low' |
| 22 | + )] |
| 23 | + [CmdletBinding()] |
| 24 | + param() |
| 25 | + |
| 26 | + # Define constants |
| 27 | + [int]$BASELINE_PROFILE_PRIORITY = 65000 |
| 28 | + |
| 29 | + #region Data Collection |
| 30 | + Write-PSFMessage '🟦 Start Prompt Shield evaluation' -Tag Test -Level VeryVerbose |
| 31 | + |
| 32 | + $activity = 'Checking Prompt Shield configuration for AI Gateway protection' |
| 33 | + Write-ZtProgress -Activity $activity -Status 'Querying prompt policies' |
| 34 | + |
| 35 | + # Q1: Get prompt policies |
| 36 | + $promptPolicies = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/promptPolicies' -QueryParameters @{ |
| 37 | + '$expand' = 'policyRules' |
| 38 | + } -ApiVersion beta |
| 39 | + |
| 40 | + # Q2: Get filtering profiles with linked policies and Conditional Access policies |
| 41 | + Write-ZtProgress -Activity $activity -Status 'Querying security profiles and linked policies' |
| 42 | + $filteringProfiles = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringProfiles' -QueryParameters @{ |
| 43 | + '$expand' = 'policies($expand=policy),conditionalAccessPolicies' |
| 44 | + } -ApiVersion beta |
| 45 | + |
| 46 | + # Get all Conditional Access policies |
| 47 | + Write-ZtProgress -Activity $activity -Status 'Querying Conditional Access policies' |
| 48 | + $allCAPolicies = Get-ZtConditionalAccessPolicy |
| 49 | + |
| 50 | + #endregion Data Collection |
| 51 | + |
| 52 | + #region Assessment Logic |
| 53 | + $enabledSecurityProfiles = @() |
| 54 | + $enabledBaselineProfiles = @() |
| 55 | + $allPromptPolicyIds = @() |
| 56 | + |
| 57 | + # Collect all prompt policy IDs from Q1 |
| 58 | + if ($promptPolicies) { |
| 59 | + $allPromptPolicyIds = $promptPolicies | ForEach-Object { $_.id } |
| 60 | + } |
| 61 | + |
| 62 | + # Find profiles linked to each prompt policy |
| 63 | + foreach ($promptPolicy in $promptPolicies) { |
| 64 | + $findParams = @{ |
| 65 | + PolicyId = $promptPolicy.id |
| 66 | + FilteringProfiles = $filteringProfiles |
| 67 | + CAPolicies = $allCAPolicies |
| 68 | + BaselinePriority = $BASELINE_PROFILE_PRIORITY |
| 69 | + PolicyLinkType = 'promptPolicyLink' |
| 70 | + PolicyRules = $promptPolicy.policyRules |
| 71 | + } |
| 72 | + |
| 73 | + $linkedProfiles = Find-ZtProfilesLinkedToPolicy @findParams |
| 74 | + |
| 75 | + foreach ($profileLink in $linkedProfiles) { |
| 76 | + if ($profileLink.ProfileType -eq 'Baseline Profile' -and $profileLink.PassesCriteria -and $profileLink.ProfileState -eq 'enabled') { |
| 77 | + $enabledBaselineProfiles += [PSCustomObject]@{ |
| 78 | + ProfileId = $profileLink.ProfileId |
| 79 | + ProfileName = $profileLink.ProfileName |
| 80 | + ProfileState = $profileLink.ProfileState |
| 81 | + ProfilePriority = $profileLink.ProfilePriority |
| 82 | + PromptPolicyId = $promptPolicy.id |
| 83 | + PromptPolicyName = $promptPolicy.name |
| 84 | + PromptPolicyAction = $promptPolicy.action |
| 85 | + RulesCount = if ($promptPolicy.policyRules) { @($promptPolicy.policyRules).Count } else { 0 } |
| 86 | + LastModified = $promptPolicy.lastModifiedDateTime |
| 87 | + PromptPolicyLinkState = $profileLink.PolicyLinkState |
| 88 | + } |
| 89 | + } |
| 90 | + elseif ($profileLink.ProfileType -eq 'Security Profile' -and $profileLink.PassesCriteria -and $profileLink.ProfileState -eq 'enabled') { |
| 91 | + $matchedCAPolicies = @() |
| 92 | + if ($null -ne $profileLink.CAPolicy) { |
| 93 | + $matchedCAPolicies = @($profileLink.CAPolicy) |
| 94 | + } |
| 95 | + |
| 96 | + $enabledSecurityProfiles += [PSCustomObject]@{ |
| 97 | + ProfileId = $profileLink.ProfileId |
| 98 | + ProfileName = $profileLink.ProfileName |
| 99 | + ProfileState = $profileLink.ProfileState |
| 100 | + ProfilePriority = $profileLink.ProfilePriority |
| 101 | + PromptPolicyId = $promptPolicy.id |
| 102 | + PromptPolicyName = $promptPolicy.name |
| 103 | + PromptPolicyAction = $promptPolicy.action |
| 104 | + RulesCount = if ($promptPolicy.policyRules) { @($promptPolicy.policyRules).Count } else { 0 } |
| 105 | + LastModified = $promptPolicy.lastModifiedDateTime |
| 106 | + PromptPolicyLinkState = $profileLink.PolicyLinkState |
| 107 | + MatchedCAPolicies = $matchedCAPolicies |
| 108 | + CAPolicyCount = $matchedCAPolicies.Count |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + $testResultMarkdown = '' |
| 114 | + $passed = $false |
| 115 | + $mdInfo = '' |
| 116 | + |
| 117 | + # Evaluation logic per spec |
| 118 | + if ($null -eq $promptPolicies -or $promptPolicies.Count -eq 0) { |
| 119 | + # No prompt policies configured |
| 120 | + $testResultMarkdown = "❌ Prompt Shield is not properly configured - no prompt policies exist.`n`n%TestResult%" |
| 121 | + $passed = $false |
| 122 | + } |
| 123 | + elseif ($enabledBaselineProfiles.Count -gt 0) { |
| 124 | + # Condition B: Baseline Profile has prompt policies (applies to all traffic) |
| 125 | + $testResultMarkdown = "✅ Prompt Shield policies are configured and enforced through the Baseline Profile which applies to all internet traffic.`n`n%TestResult%" |
| 126 | + $passed = $true |
| 127 | + } |
| 128 | + elseif ($enabledSecurityProfiles.Count -gt 0) { |
| 129 | + # Condition A: Security profiles with prompt policies AND CA policy assignment |
| 130 | + $testResultMarkdown = "✅ Prompt Shield policies are configured and enforced through security profiles assigned to Conditional Access policies.`n`n%TestResult%" |
| 131 | + $passed = $true |
| 132 | + } |
| 133 | + else { |
| 134 | + # Prompt policies exist but are not enforced |
| 135 | + $testResultMarkdown = "❌ Prompt Shield is not properly configured - policies are not linked to security profiles, or security profiles with prompt policies are not enforced (no CA policy assignment and not using Baseline Profile).`n`n%TestResult%" |
| 136 | + $passed = $false |
| 137 | + } |
| 138 | + |
| 139 | + #endregion Assessment Logic |
| 140 | + |
| 141 | + #region Report Generation |
| 142 | + |
| 143 | + if ($passed) { |
| 144 | + # Build detailed report only when test passes |
| 145 | + $formatTemplate = @' |
| 146 | +
|
| 147 | +## Prompt Policies (AI Gateway) |
| 148 | +
|
| 149 | +| Policy Name | Action | Rules Count | Last Modified | |
| 150 | +| :---------- | :----- | :---------- | :------------ | |
| 151 | +{0} |
| 152 | +{1} |
| 153 | +{2} |
| 154 | +'@ |
| 155 | + |
| 156 | + # Table 1: Prompt Policies |
| 157 | + $promptPoliciesRows = '' |
| 158 | + if ($promptPolicies -and $promptPolicies.Count -gt 0) { |
| 159 | + foreach ($policy in $promptPolicies) { |
| 160 | + $policyPortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditPromptPolicyMenuBlade.MenuView/~/basics/policyId/$($policy.id)" |
| 161 | + $policyName = Get-SafeMarkdown -Text $policy.name |
| 162 | + $action = if ($policy.action) { $policy.action } else { 'Not specified' } |
| 163 | + $rulesCount = if ($policy.policyRules) { @($policy.policyRules).Count } else { 0 } |
| 164 | + $lastModified = if ($policy.lastModifiedDateTime) { $policy.lastModifiedDateTime } else { 'N/A' } |
| 165 | + $promptPoliciesRows += "| [$policyName]($policyPortalLink) | $action | $rulesCount | $lastModified |`n" |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + # Table 2: Baseline Profiles with Prompt Policies |
| 170 | + $baselineProfilesSection = '' |
| 171 | + if ($enabledBaselineProfiles.Count -gt 0) { |
| 172 | + $baselineProfilesSection += "`n## Prompt Policies Linked to Baseline Profile`n`n" |
| 173 | + $baselineProfilesSection += "| Profile Name | Priority | State | Prompt Policy | Policy Link State | Rules Count |`n" |
| 174 | + $baselineProfilesSection += "| :----------- | :------- | :---- | :------------ | :---------------- | :---------- |`n" |
| 175 | + foreach ($profile in $enabledBaselineProfiles) { |
| 176 | + $profilePortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$($profile.ProfileId)" |
| 177 | + $policyPortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditPromptPolicyMenuBlade.MenuView/~/basics/policyId/$($profile.PromptPolicyId)" |
| 178 | + $profileName = Get-SafeMarkdown -Text $profile.ProfileName |
| 179 | + $policyName = Get-SafeMarkdown -Text $profile.PromptPolicyName |
| 180 | + $baselineProfilesSection += "| [$profileName]($profilePortalLink) | $($profile.ProfilePriority) | $($profile.ProfileState) | [$policyName]($policyPortalLink) | $($profile.PromptPolicyLinkState) | $($profile.RulesCount) |`n" |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + # Table 3: Security Profiles with Prompt Policies and CA Assignments |
| 185 | + $securityProfilesSection = '' |
| 186 | + if ($enabledSecurityProfiles.Count -gt 0) { |
| 187 | + $securityProfilesSection += "`n## Security Profiles with Linked Policies`n`n" |
| 188 | + $securityProfilesSection += "| Profile Name | State | Priority | Prompt Policy | CA Policies Assigned | Is Baseline |`n" |
| 189 | + $securityProfilesSection += "| :----------- | :---- | :------- | :------------ | :------------------- | :---------- |`n" |
| 190 | + foreach ($profile in $enabledSecurityProfiles) { |
| 191 | + $profilePortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$($profile.ProfileId)" |
| 192 | + $policyPortalLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditPromptPolicyMenuBlade.MenuView/~/basics/policyId/$($profile.PromptPolicyId)" |
| 193 | + $profileName = Get-SafeMarkdown -Text $profile.ProfileName |
| 194 | + $policyName = Get-SafeMarkdown -Text $profile.PromptPolicyName |
| 195 | + $isBaseline = if ($profile.ProfilePriority -eq $BASELINE_PROFILE_PRIORITY) { 'Yes' } else { 'No' } |
| 196 | + $caCount = $profile.CAPolicyCount |
| 197 | + $securityProfilesSection += "| [$profileName]($profilePortalLink) | $($profile.ProfileState) | $($profile.ProfilePriority) | [$policyName]($policyPortalLink) | $caCount | $isBaseline |`n" |
| 198 | + } |
| 199 | + |
| 200 | + # Table 4: Conditional Access Policies |
| 201 | + $securityProfilesSection += "`n## Conditional Access Policies Assigned to Security Profiles`n`n" |
| 202 | + $securityProfilesSection += "| CA Policy Name | Security Profile | CA Policy ID |`n" |
| 203 | + $securityProfilesSection += "| :------------- | :--------------- | :----------- |`n" |
| 204 | + foreach ($profile in $enabledSecurityProfiles) { |
| 205 | + foreach ($caPolicy in $profile.MatchedCAPolicies) { |
| 206 | + $caPolicyPortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($caPolicy.Id)" |
| 207 | + $profileName = Get-SafeMarkdown -Text $profile.ProfileName |
| 208 | + $caPolicyName = Get-SafeMarkdown -Text $caPolicy.DisplayName |
| 209 | + $securityProfilesSection += "| [$caPolicyName]($caPolicyPortalLink) | $profileName | $($caPolicy.Id) |`n" |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + $mdInfo = $formatTemplate -f $promptPoliciesRows, $baselineProfilesSection, $securityProfilesSection |
| 215 | + } |
| 216 | + else { |
| 217 | + # For failed states, just show the portal link |
| 218 | + $mdInfo = "[View Prompt Shield Configuration](https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/PromptPolicy.ReactView)`n" |
| 219 | + } |
| 220 | + |
| 221 | + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo |
| 222 | + |
| 223 | + #endregion Report Generation |
| 224 | + |
| 225 | + $params = @{ |
| 226 | + TestId = '25415' |
| 227 | + Status = $passed |
| 228 | + Result = $testResultMarkdown |
| 229 | + } |
| 230 | + |
| 231 | + Add-ZtTestResultDetail @params |
| 232 | +} |
0 commit comments