Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
725163d
added test
ashwinikarke Feb 9, 2026
7b9a142
added test
ashwinikarke Feb 11, 2026
c2b1072
added test
ashwinikarke Feb 11, 2026
450ace1
added test
ashwinikarke Feb 11, 2026
41d78d4
resolved copilot comments
ashwinikarke Feb 11, 2026
04821b0
updated code
ashwinikarke Feb 13, 2026
44f2626
updated code
ashwinikarke Feb 13, 2026
de7ed5a
resolved copilot comments
ashwinikarke Feb 13, 2026
180eb4f
Trigger validation with current main
alexandair Feb 13, 2026
cefdbc3
Remove backticks in Invoke-ZtGraphRequest call for subscribed SKUs
alexandair Feb 15, 2026
2e4265b
taken pull
ashwinikarke Feb 16, 2026
b217980
taken pull
ashwinikarke Feb 16, 2026
0909429
used database query
ashwinikarke Feb 16, 2026
e417e77
Add test files for 26887
aahmed-spec Feb 17, 2026
31d6a86
Made changes as per copilot suggestions
aahmed-spec Feb 17, 2026
c80293c
Merge branch 'main' of https://github.com/microsoft/zerotrustassessme…
aahmed-spec Feb 18, 2026
c1484eb
resolved PR comments
ashwinikarke Feb 18, 2026
4e08d89
Feature-25533
Manoj-Kesana Feb 19, 2026
6270ead
Made changes as per Alek's suggestions
aahmed-spec Feb 19, 2026
1de2172
Made changes as per Copilot suggestions
aahmed-spec Feb 19, 2026
ce8a573
test-26888
aahmed-spec Feb 19, 2026
89b5195
Delete src/powershell/tests/Test-Assessment.26887.ps1
aahmed-spec Feb 19, 2026
9c2ab11
Delete src/powershell/tests/Test-Assessment.26887.md
aahmed-spec Feb 19, 2026
6203efa
Fix casing in Azure Resource Graph query for consistency
alexandair Feb 19, 2026
179ea0d
Fix formatting of remediation action links in Test-Assessment.26888.md
alexandair Feb 19, 2026
611f3da
Fix casing inconsistencies in resource type and update table header i…
alexandair Feb 19, 2026
794abcc
taken pull of main
ashwinikarke Feb 20, 2026
ddffa50
updated db query
ashwinikarke Feb 20, 2026
899e1d5
Merge pull request #922 from microsoft/test-26887-fresh
SagarSathe Feb 20, 2026
d9aa7b6
Merge pull request #925 from microsoft/test-26888
SagarSathe Feb 20, 2026
3a16f6b
Merge pull request #879 from microsoft/Feature-25375
SagarSathe Feb 20, 2026
d96c0b7
Feature-25533
Manoj-Kesana Feb 19, 2026
6e788a1
Feedback Addressed
Manoj-Kesana Feb 20, 2026
e5c5ca9
Update src/powershell/tests/Test-Assessment.25533.ps1
Manoj-Kesana Feb 20, 2026
eb9a837
Merge branch 'Feature-25533' of https://github.com/microsoft/zerotrus…
Manoj-Kesana Feb 20, 2026
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
12 changes: 12 additions & 0 deletions src/powershell/tests/Test-Assessment.25375.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Global Secure Access requires specific Microsoft Entra licenses to function, including Microsoft Entra Internet Access and Microsoft Entra Private Access, both of which require Microsoft Entra ID P1 as a prerequisite. Without valid GSA licenses provisioned in the tenant, administrators cannot configure traffic forwarding profiles, security policies, or remote network connections. If licenses exist but are not assigned to users, those users will not have their traffic routed through the Global Secure Access service, leaving them unprotected by the security controls configured in the platform. Threat actors targeting unprotected users can bypass web content filtering, threat protection, and conditional access policies that would otherwise apply through GSA. Additionally, if licenses are assigned but the subscription has expired or been suspended, the entire GSA infrastructure becomes non-functional, creating a sudden security gap where previously protected traffic flows unmonitored. This check verifies that valid GSA licenses exist in the tenant with an enabled capability status and that those licenses are actively assigned to users who require protection through the Global Secure Access service.

**Remediation action**

- [Review GSA licensing requirements and purchase appropriate licenses](https://learn.microsoft.com/en-us/entra/global-secure-access/overview-what-is-global-secure-access#licensing-overview)
- [Assign licenses to users through Microsoft Entra admin center](https://learn.microsoft.com/en-us/entra/fundamentals/license-users-groups)
- [Use group-based licensing for easier management at scale](https://learn.microsoft.com/en-us/entra/fundamentals/concept-group-based-licensing)
- [Monitor license utilization through Microsoft 365 admin center](https://admin.microsoft.com/Adminportal/Home#/licenses)
- [Review Microsoft Entra Suite as an alternative that includes both Internet Access and Private Access](https://learn.microsoft.com/en-us/entra/fundamentals/whats-new#microsoft-entra-suite)

<!--- Results --->
%TestResult%
339 changes: 339 additions & 0 deletions src/powershell/tests/Test-Assessment.25375.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
<#
.SYNOPSIS
Validates that GSA licenses are available in the tenant and assigned to users.

.DESCRIPTION
This test checks whether Global Secure Access (GSA) licenses are provisioned in the tenant
and actively assigned to users. It verifies:
- GSA service plans exist in tenant subscribed SKUs
- Licenses have capabilityStatus = "Enabled"
- Licenses are assigned to at least one user
- Service plans are not disabled for assigned users

.NOTES
Test ID: 25375
Category: Global Secure Access
Required API: subscribedSkus (beta)
Required Database: User table
GSA Service Plan IDs:
- Entra_Premium_Internet_Access: 8d23cb83-ab07-418f-8517-d7aca77307dc
- Entra_Premium_Private_Access: f057aab1-b184-49b2-85c0-881b02a405c5
#>

function Test-Assessment-25375 {
[ZtTest(
Category = 'Global Secure Access',
ImplementationCost = 'Low',
MinimumLicense = ('Entra_Premium_Internet_Access', 'Entra_Premium_Private_Access'),
Pillar = 'Network',
RiskLevel = 'High',
SfiPillar = 'Protect networks',
TenantType = ('Workforce'),
TestId = 25375,
Title = 'GSA Licenses are available in the tenant and assigned to users',
UserImpact = 'Low'
)]
[CmdletBinding()]
param(
$Database
)

#region Data Collection
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose

$activity = 'Checking GSA license availability and assignment'
Write-ZtProgress -Activity $activity -Status 'Querying tenant licenses'

# GSA Service Plan IDs
$gsaServicePlanIds = @{
InternetAccess = '8d23cb83-ab07-418f-8517-d7aca77307dc' # Entra_Premium_Internet_Access
PrivateAccess = 'f057aab1-b184-49b2-85c0-881b02a405c5' # Entra_Premium_Private_Access
}

$skuCmdletFailed = $false
$userCmdletFailed = $false
$subscribedSkus = @()
$userLicenses = @()

# Query 1: Retrieve tenant licenses with GSA service plans
try {
$subscribedSkus = Invoke-ZtGraphRequest -RelativeUri 'subscribedSkus' -ApiVersion beta -ErrorAction Stop
}
catch {
$skuCmdletFailed = $true
Write-PSFMessage "Failed to retrieve subscribed SKUs: $_" -Tag Test -Level Warning
}

Write-ZtProgress -Activity $activity -Status 'Querying user license assignments'

# Query 2: Retrieve all users with assigned licenses from database
try {
$sqlUsers = @"
SELECT
u.id,
u.displayName,
u.userPrincipalName,
unnest(u.assignedLicenses).skuId::VARCHAR AS skuId,
unnest(u.assignedLicenses).disabledPlans AS disabledPlans
FROM "User" u
WHERE len(u.assignedLicenses) > 0
"@
$userLicenses = @(Invoke-DatabaseQuery -Database $Database -Sql $sqlUsers -AsCustomObject -ErrorAction Stop)
# Filter out any records with null IDs
$userLicenses = @($userLicenses | Where-Object { $_.id })
}
catch {
$userCmdletFailed = $true
Write-PSFMessage "Failed to retrieve users: $_" -Tag Test -Level Warning
}
#endregion Data Collection

#region Assessment Logic
$testResultMarkdown = ''
$passed = $false
$customStatus = $null

# Handle any query failure - cannot determine license status
if ($skuCmdletFailed -or $userCmdletFailed) {
Write-PSFMessage "Unable to retrieve GSA license data due to query failure" -Tag Test -Level Warning
$customStatus = 'Investigate'
$testResultMarkdown = "⚠️ Unable to determine GSA license availability and assignment due to query failure, connection issues, or insufficient permissions.`n`n"

Add-ZtTestResultDetail -TestId '25375' -Title 'GSA Licenses are available in the tenant and assigned to users' -Status $false -Result $testResultMarkdown -CustomStatus $customStatus
return
}

# Filter SKUs containing GSA service plans
$gsaSkus = @($subscribedSkus | Where-Object {
$_.ServicePlans | Where-Object { $_.ServicePlanId -in $gsaServicePlanIds.Values }
})

# Check if GSA licenses exist and are enabled
$enabledGsaSkus = @($gsaSkus | Where-Object { $_.CapabilityStatus -eq 'Enabled' })

if ($gsaSkus.Count -eq 0 -or $enabledGsaSkus.Count -eq 0) {
# No GSA licenses available or not enabled - skip test
Write-PSFMessage 'No GSA licenses are available in this tenant.' -Tag Test -Level Verbose
Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'No GSA licenses are available in this tenant.'
return
}

Write-ZtProgress -Activity $activity -Status 'Analyzing user license assignments'

# Build SKU ID to SKU mapping and pre-filter service plans for performance
$gsaSkuIds = @{}
$internetAccessPlansBySku = @{}
$privateAccessPlansBySku = @{}

foreach ($sku in $enabledGsaSkus) {
$skuIdString = $sku.SkuId.ToString().ToLower()
$gsaSkuIds[$skuIdString] = $sku

# Pre-filter service plans to avoid repeated Where-Object calls
$internetPlan = $sku.ServicePlans | Where-Object { $_.ServicePlanId -eq $gsaServicePlanIds.InternetAccess }
if ($internetPlan) {
$internetAccessPlansBySku[$skuIdString] = $internetPlan
}

$privatePlan = $sku.ServicePlans | Where-Object { $_.ServicePlanId -eq $gsaServicePlanIds.PrivateAccess }
if ($privatePlan) {
$privateAccessPlansBySku[$skuIdString] = $privatePlan
}
}

# Count users with GSA licenses assigned
$usersWithInternetAccess = [System.Collections.Generic.List[object]]::new()
$usersWithPrivateAccess = [System.Collections.Generic.List[object]]::new()
$usersWithAnyGsa = [System.Collections.Generic.List[object]]::new()

# Group licenses by user (since query returns one row per license)
$userGroups = $userLicenses | Group-Object -Property id

foreach ($userGroup in $userGroups) {
$userId = $userGroup.Name
$userLicenseRecords = $userGroup.Group
$userDisplayName = $userLicenseRecords[0].displayName
$userPrincipalName = $userLicenseRecords[0].userPrincipalName

$hasInternetAccess = $false
$hasPrivateAccess = $false

foreach ($licenseRecord in $userLicenseRecords) {
if (-not $licenseRecord.skuId) { continue }

$userSkuId = $licenseRecord.skuId.ToString().ToLower()

if ($gsaSkuIds.ContainsKey($userSkuId)) {
$disabledPlans = if ($licenseRecord.disabledPlans) { $licenseRecord.disabledPlans } else { @() }

# Check if Internet Access service plan is enabled
if ($internetAccessPlansBySku.ContainsKey($userSkuId)) {
$internetPlan = $internetAccessPlansBySku[$userSkuId]
if ($internetPlan.ServicePlanId -notin $disabledPlans) {
$hasInternetAccess = $true
}
}

# Check if Private Access service plan is enabled
if ($privateAccessPlansBySku.ContainsKey($userSkuId)) {
$privatePlan = $privateAccessPlansBySku[$userSkuId]
if ($privatePlan.ServicePlanId -notin $disabledPlans) {
$hasPrivateAccess = $true
}
}
}
}

# Create user object for display
$userObj = [PSCustomObject]@{
Id = $userId
DisplayName = $userDisplayName
UserPrincipalName = $userPrincipalName
}

if ($hasInternetAccess) {
$usersWithInternetAccess.Add($userObj)
}
if ($hasPrivateAccess) {
$usersWithPrivateAccess.Add($userObj)
}
if ($hasInternetAccess -or $hasPrivateAccess) {
$usersWithAnyGsa.Add($userObj)
}
}

$gsaUserCount = $usersWithAnyGsa.Count

# Evaluate test result
if ($gsaUserCount -eq 0) {
# Licenses exist and enabled but not assigned to any user - fail
$passed = $false
$testResultMarkdown = "❌ GSA licenses are available in the tenant but not assigned to any user.`n`n%TestResult%"
}
else {
# Licenses exist, enabled, and assigned to at least one user - pass
$passed = $true
$testResultMarkdown = "✅ GSA licenses are available and assigned to at least one user.`n`n%TestResult%"
}
#endregion Assessment Logic

#region Report Generation
# Build detailed information if we have valid license data
$mdInfo = ''

if ($null -ne $enabledGsaSkus -and $enabledGsaSkus.Count -gt 0) {
$reportTitle = 'Licenses'
$portalLink = 'https://admin.microsoft.com/Adminportal/Home#/licenses'

$formatTemplate = @'

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

**GSA License Summary:**

| SKU Name | Status | Available | Assigned |
| :------- | :----- | --------: | -------: |
{2}

**GSA Service Plans Detected:**

| Service Plan | SKU |
| :----------- | :-- |
{3}

**User Assignment Summary:**

| Metric | Value |
| :----- | ----: |
{4}

{5}
'@

# Build SKU table
$skuTableRows = ''
foreach ($sku in $enabledGsaSkus) {
$skuName = Get-SafeMarkdown -Text $sku.SkuPartNumber
$status = Get-SafeMarkdown -Text $sku.CapabilityStatus
$available = $sku.PrepaidUnits.Enabled
$assigned = $sku.ConsumedUnits

$skuTableRows += "| $skuName | $status | $available | $assigned |`n"
}

# Build service plan table
$servicePlanTableRows = ''
foreach ($sku in $enabledGsaSkus) {
$gsaPlans = $sku.ServicePlans | Where-Object { $_.ServicePlanId -in $gsaServicePlanIds.Values }
foreach ($plan in $gsaPlans) {
$planName = Get-SafeMarkdown -Text $plan.ServicePlanName
$skuName = Get-SafeMarkdown -Text $sku.SkuPartNumber

$servicePlanTableRows += "| $planName | $skuName |`n"
}
}

# Build user assignment summary
$assignmentSummary = "| Users with GSA Internet Access | $($usersWithInternetAccess.Count) |`n"
$assignmentSummary += "| Users with GSA Private Access | $($usersWithPrivateAccess.Count) |`n"
$assignmentSummary += "| Total users with any GSA license | $gsaUserCount |`n"

# Build user list (truncate at 10)
$userListSection = ''
if ($gsaUserCount -gt 0) {
if ($gsaUserCount -gt 10) {
$userListSection += "**Users with GSA licenses (Showing 10 of $gsaUserCount):**`n`n"
}
else {
$userListSection += "**Users with GSA licenses:**`n`n"
}

$userListSection += "| Display name | User principal name | Internet Access | Private Access |`n"
$userListSection += "| :----------- | :------------------ | :-------------- | :------------- |`n"

# Build HashSets for efficient ID lookups
if ($usersWithInternetAccess.Count -gt 0) {
$internetAccessIds = [System.Collections.Generic.HashSet[string]]::new([string[]]($usersWithInternetAccess.Id))
} else {
$internetAccessIds = [System.Collections.Generic.HashSet[string]]::new()
}

if ($usersWithPrivateAccess.Count -gt 0) {
$privateAccessIds = [System.Collections.Generic.HashSet[string]]::new([string[]]($usersWithPrivateAccess.Id))
} else {
$privateAccessIds = [System.Collections.Generic.HashSet[string]]::new()
}

$displayUsers = $usersWithAnyGsa | Select-Object -First 10
foreach ($user in $displayUsers) {
$displayName = Get-SafeMarkdown -Text $user.DisplayName
$upn = Get-SafeMarkdown -Text $user.UserPrincipalName
$hasInternet = if ($internetAccessIds.Contains($user.Id)) { '✅' } else { '❌' }
$hasPrivate = if ($privateAccessIds.Contains($user.Id)) { '✅' } else { '❌' }

$userListSection += "| $displayName | $upn | $hasInternet | $hasPrivate |`n"
}

if ($gsaUserCount -gt 10) {
$userListSection += "| ... | | | |`n`n"
$userListSection += "View all users in [Microsoft 365 admin center - Licenses](https://admin.microsoft.com/Adminportal/Home#/licenses)"
}
}

$mdInfo = $formatTemplate -f $reportTitle, $portalLink, $skuTableRows, $servicePlanTableRows, $assignmentSummary, $userListSection
}

$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
#endregion Report Generation

$params = @{
TestId = '25375'
Title = 'GSA Licenses are available in the tenant and assigned to users'
Status = $passed
Result = $testResultMarkdown
}
if ($customStatus) {
$params.CustomStatus = $customStatus
}
Add-ZtTestResultDetail @params
}
16 changes: 16 additions & 0 deletions src/powershell/tests/Test-Assessment.25533.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
DDoS attacks remain a major security and availability risk for customers with cloud-hosted applications. These attacks aim to overwhelm an application's compute, network, or memory resources, rendering it inaccessible to legitimate users. Any public-facing endpoint exposed to the internet can be a potential target for a DDoS attack. Azure DDoS Protection provides always-on monitoring and automatic mitigation against DDoS attacks targeting public-facing workloads. Without Azure DDoS Protection (Network Protection or IP Protection), public IP addresses for services such as Application Gateways, Load Balancers, Azure Firewalls, Azure Bastion, Virtual Network Gateways, or virtual machines remain exposed to DDoS attacks that can overwhelm network bandwidth, exhaust system resources, and cause complete service unavailability. These attacks can disrupt access for legitimate users, degrade performance, and create cascading outages across dependent services. Azure DDoS Protection can be enabled in two ways:
DDoS IP Protection — Protection is explicitly enabled on individual public IP addresses by setting ddosSettings.protectionMode to Enabled.
DDoS Network Protection — Protection is enabled at the VNET level through a DDoS Protection Plan. Public IP addresses associated with resources in that VNET inherit the protection when ddosSettings.protectionMode is set to VirtualNetworkInherited. However, a public IP address with VirtualNetworkInherited is not protected unless the VNET actually has a DDoS Protection Plan associated and enableDdosProtection set to true.
This check verifies that every public IP address is actually covered by DDoS protection, either through DDoS IP Protection enabled directly on the public IP, or through DDoS Network Protection enabled on the VNET that the public IP's associated resource resides in. If this check does not pass, your workloads remain significantly more vulnerable to downtime, customer impact, and operational disruption during an attack.

**Remediation action**

To enable DDoS Protection for public IP addresses, refer to the following Microsoft Learn documentation:

- [Azure DDoS Protection overview](https://learn.microsoft.com/en-us/azure/ddos-protection/ddos-protection-overview)
- [Quickstart: Create and configure Azure DDoS Network Protection using Azure portal](https://learn.microsoft.com/en-us/azure/ddos-protection/manage-ddos-protection)
- [Quickstart: Create and configure Azure DDoS IP Protection using Azure portal](https://learn.microsoft.com/en-us/azure/ddos-protection/manage-ddos-ip-protection-portal)
- [Azure DDoS Protection SKU comparison](https://learn.microsoft.com/en-us/azure/ddos-protection/ddos-protection-sku-comparison)

<!--- Results --->
%TestResult%
Loading