tests/Test-Assessment.25422.ps1

<#
.SYNOPSIS
    GSA Deployment logs are populated and reviewed
 
.DESCRIPTION
    Verifies that Global Secure Access deployment logs are being populated and that recent deployments
    have succeeded. Failed deployments indicate configuration issues that require investigation.
 
.NOTES
    Test ID: 25422
    Pillar: Network
    Risk Level: Medium
    SFI Pillar: Monitor and detect cyberthreats
#>


function Test-Assessment-25422 {
    [ZtTest(
        Category = 'Global Secure Access',
        ImplementationCost = 'Low',
        MinimumLicense = ('Entra_Premium_Internet_Access','Entra_Premium_Private_Access'),
        Pillar = 'Network',
        RiskLevel = 'Medium',
        SfiPillar = 'Monitor and detect cyberthreats',
        TenantType = ('Workforce', 'External'),
        TestId = '25422',
        Title = 'GSA Deployment logs are populated and reviewed',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection
    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    $activity = 'Checking GSA Deployment logs are populated and reviewed'

    # Q1: Check if GSA is enabled (prerequisite check)
    Write-ZtProgress -Activity $activity -Status 'Checking if Global Secure Access is enabled'
    $forwardingProfiles = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/forwardingProfiles' -ApiVersion beta

    # Check if GSA is configured
    $gsaEnabled = $false
    if ($forwardingProfiles -and $forwardingProfiles.Count -gt 0) {
        $enabledProfiles = $forwardingProfiles | Where-Object { $_.state -eq 'enabled' }
        $gsaEnabled = $enabledProfiles.Count -gt 0
    }

    # If GSA not configured, skip
    if (-not $gsaEnabled) {
        Write-PSFMessage 'Global Secure Access is not enabled in this tenant.' -Tag Test -Level Verbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable
        return
    }

    # Q2: Retrieve GSA deployment logs
    Write-ZtProgress -Activity $activity -Status 'Retrieving deployment logs'
    $deployments = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/deployments' -ApiVersion beta

    # Filter deployments from the last 30 days
    $thirtyDaysAgo = (Get-Date).AddDays(-30)
    $recentDeployments = @()

    if ($deployments -and $deployments.Count -gt 0) {
        $recentDeployments = $deployments | Where-Object {
            $deploymentDate = $null
            if ($_.deploymentEndDateTime) {
                $deploymentDate = [datetime]$_.deploymentEndDateTime
            }
            elseif ($_.lastModifiedDateTime) {
                $deploymentDate = [datetime]$_.lastModifiedDateTime
            }
            $deploymentDate -and $deploymentDate -ge $thirtyDaysAgo
        }
    }

    # Calculate deployment statistics
    $totalCount = if ($recentDeployments) { @($recentDeployments).Count } else { 0 }
    $succeededCount = @($recentDeployments | Where-Object { $_.status.deploymentStage -eq 'succeeded' }).Count
    $failedCount = @($recentDeployments | Where-Object { $_.status.deploymentStage -eq 'failed' }).Count
    $inProgressCount = @($recentDeployments | Where-Object { $_.status.deploymentStage -in @('pending', 'inProgress') }).Count
    $failureRate = if ($totalCount -gt 0) { [math]::Round(($failedCount / $totalCount) * 100, 1) } else { 0 }
    #endregion Data Collection

    #region Assessment Logic
    $passed = $false

    # If failedCount = 0 → Pass
    # If failedCount > 0 → Fail
    if ($failedCount -eq 0) {
        $passed = $true
        $testResultMarkdown = "GSA deployment logs are populated and recent deployments have succeeded.`n`n%TestResult%"
    }
    else {
        $passed = $false
        $testResultMarkdown = "GSA deployment logs contain failed deployments that require investigation.`n`n%TestResult%"
    }
    #endregion Assessment Logic

    #region Report Generation
    $mdInfo = ''

    # Build deployment summary table
    $deploymentLogsLink = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/DeploymentLogs.ReactView'

    $mdInfo += @"
 
## [Deployment Logs]($deploymentLogsLink)
 
**Deployment Summary (Last 30 Days):**
 
| Metric | Value |
| :--- | :--- |
| Total Deployments | $totalCount |
| Succeeded | $succeededCount |
| Failed | $failedCount |
| In Progress | $inProgressCount |
| Failure Rate | $failureRate% |
 
"@


    # Build recent deployments table
    if ($totalCount -gt 0) {
        # Check if truncation needed
        $displayCount = [math]::Min($totalCount, 10)
        $isTruncated = $totalCount -gt 10

        $truncationMessage = ''
        if ($isTruncated) {
            $truncationMessage = "Showing $displayCount of $totalCount deployments. [View all deployments]($deploymentLogsLink)`n`n"
        }

        $formatTemplate = @'
 
**Recent Deployments:**
 
{0}
| Date | Operation | Change Type | Status | Initiated By | Error Message |
| :--- | :--- | :--- | :--- | :--- | :--- |
{1}
 
'@


        $tableRows = ''

        # Sort by date descending and take first 10
        $sortedDeployments = $recentDeployments | Sort-Object -Property {
            if ($_.deploymentEndDateTime) { [datetime]$_.deploymentEndDateTime }
            elseif ($_.lastModifiedDateTime) { [datetime]$_.lastModifiedDateTime }
            else { [datetime]::MinValue }
        } -Descending | Select-Object -First 10

        foreach ($deployment in $sortedDeployments) {
            $deploymentDate = if ($deployment.deploymentEndDateTime) {
                ([datetime]$deployment.deploymentEndDateTime).ToString('yyyy-MM-dd HH:mm')
            }
            elseif ($deployment.lastModifiedDateTime) {
                ([datetime]$deployment.lastModifiedDateTime).ToString('yyyy-MM-dd HH:mm')
            }
            else {
                'N/A'
            }

            $operationName = if ($deployment.configuration.operationName) {
                Get-SafeMarkdown -Text $deployment.configuration.operationName
            }
            else {
                'N/A'
            }

            $changeType = if ($deployment.configuration.changeType) {
                $deployment.configuration.changeType
            }
            else {
                'N/A'
            }

            $stage = $deployment.status.deploymentStage
            $icon = switch ($stage) {
                'succeeded' { '✅' }
                'failed' { '❌' }
                'inProgress' { '🔄' }
                'pending' { '⏳' }
                default { '' }
            }
            $deploymentStage = switch ($stage) {
                'succeeded' { 'Succeeded' }
                'failed' { 'Failed' }
                'inProgress' { 'In Progress' }
                'pending' { 'Pending' }
                default { $stage }
            }
            $resultText = if ($icon) { "$icon $deploymentStage" } else { $deploymentStage }

            $initiatedBy = if ($deployment.initiatedBy) {
                Get-SafeMarkdown -Text $deployment.initiatedBy
            }
            else {
                'N/A'
            }

            $errorMessage = if ($deployment.status.message) {
                Get-SafeMarkdown -Text $deployment.status.message
            }
            else {
                'N/A'
            }

            $tableRows += "| $deploymentDate | $operationName | $changeType | $resultText | $initiatedBy | $errorMessage |`n"
        }

        if ($isTruncated) {
            $tableRows += "| ... | | | | | |`n"
        }

        $mdInfo += $formatTemplate -f $truncationMessage, $tableRows
    }
    else {
        $mdInfo += "`n**Recent Deployments:**`n`nNo deployments found in the last 30 days.`n"
    }

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

    $params = @{
        TestId = '25422'
        Title  = 'GSA Deployment logs are populated and reviewed'
        Status = $passed
        Result = $testResultMarkdown
    }

    Add-ZtTestResultDetail @params
}