tests/Test-Assessment.41011.ps1
|
<#
.SYNOPSIS Non-administrative accounts do not have DC-Sync (Directory Replication) permissions .NOTES Test ID: 41011 Workshop Task: SECOPS-011 Pillar: SecOps Category: Identity threat protection Required permission: SecurityEvents.Read.All #> function Test-Assessment-41011 { [ZtTest( Category = 'Identity threat protection', CompatibleLicense = ('ATA'), ImplementationCost = 'Low', Pillar = 'SecOps', RiskLevel = 'High', Service = ('Graph'), SfiPillar = 'Protect identities and secrets', TenantType = ('Workforce'), TestId = 41011, Title = 'Non-administrative accounts do not have DC-Sync (Directory Replication) permissions', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking non-admin DC-Sync permissions' $title = 'Non-administrative accounts do not have DC-Sync (Directory Replication) permissions' $investigateParams = @{ TestId = '41011' Title = $title Status = $false CustomStatus = 'Investigate' Result = '⚠️ The Secure Score control profile or latest Secure Score snapshot could not be read due to a permission or connectivity error. Verify the caller has SecurityEvents.Read.All (Entra role: Security Reader) and re-run.' } # Q1: Retrieve the MDI posture control profile for non-admin DC-Sync accounts. # The service filter must use the legacy literal 'Azure ATP', not the human-readable display name. Write-ZtProgress -Activity $activity -Status 'Retrieving Secure Score control profile' $controlProfile = $null try { $q1Filter = "service eq 'Azure ATP' and id eq 'AATP_NonAdminDCSyncAccounts'" $profileResults = @(Invoke-ZtGraphRequest -RelativeUri 'security/secureScoreControlProfiles' -Filter $q1Filter -ApiVersion beta -ErrorAction Stop) $controlProfile = $profileResults | Select-Object -First 1 } catch { $statusCode = Get-ZtHttpStatusCode -ErrorRecord $_ Write-PSFMessage "Failed to retrieve the DC-Sync Secure Score control profile: $(Get-ZtSafeErrorMessage -ErrorRecord $_)" -Tag Test -Level Warning if ($statusCode -in (401, 403)) { $investigateParams.Result = '⚠️ The Secure Score control profile could not be read because the request was not authorized. Verify the caller has the SecurityEvents.Read.All permission (Entra role: Security Reader) and re-run.' Add-ZtTestResultDetail @investigateParams return } Add-ZtTestResultDetail @investigateParams return } $latestScore = $null if ($null -ne $controlProfile) { # Q2: Retrieve the latest Secure Score snapshot to read per-control scores. # -DisablePaging returns the raw Graph response wrapper; unwrap .value to get the snapshot. Write-ZtProgress -Activity $activity -Status 'Retrieving latest Secure Score' try { $scoreResponse = Invoke-ZtGraphRequest -RelativeUri 'security/secureScores' -ApiVersion beta -Top 1 -DisablePaging -ErrorAction Stop $latestScore = $scoreResponse.value | Select-Object -First 1 } catch { Write-PSFMessage "Failed to retrieve the latest Secure Score snapshot: $(Get-ZtSafeErrorMessage -ErrorRecord $_)" -Tag Test -Level Warning Add-ZtTestResultDetail @investigateParams return } } #endregion Data Collection #region Assessment Logic $passed = $false # Investigate: MDI posture assessment not surfaced in this tenant's Secure Score. if ($null -eq $controlProfile) { $investigateParams.Result = 'The Microsoft Defender for Identity posture recommendation for non-admin Directory Replication permissions was not found in the tenant''s Microsoft Secure Score.' Add-ZtTestResultDetail @investigateParams return } $controlId = $controlProfile.id $controlTitle = $controlProfile.title $maxScore = $controlProfile.maxScore $actionUrl = $controlProfile.actionUrl # Determine the most recent control state. An empty controlStateUpdates array means Default. $latestStateUpdate = @( $controlProfile.controlStateUpdates | Sort-Object { if ($_.updatedDateTime) { [datetime]$_.updatedDateTime } else { [datetime]::MinValue } } -Descending | Select-Object -First 1 )[0] $latestState = if ($null -ne $latestStateUpdate -and $null -ne $latestStateUpdate.state) { $latestStateUpdate.state } else { 'Default' } # Investigate: admin has explicitly accepted risk; human confirmation is required before treating this as pass or fail. if ($latestState -eq 'Ignored') { $investigateParams.Result = 'The Microsoft Defender for Identity posture recommendation for non-admin Directory Replication permissions has been marked **Ignored** in Secure Score (risk explicitly accepted — confirm the exposed-account list in the Defender XDR portal).' Add-ZtTestResultDetail @investigateParams return } # Investigate: Secure Score snapshot unavailable after successful Q1. if ($null -eq $latestScore) { $investigateParams.Result = '⚠️ No Microsoft Secure Score snapshot was returned. Re-run after 5-10 minutes.' Add-ZtTestResultDetail @investigateParams return } # Locate the per-control score entry by matching controlName to the profile ID from Q1. $controlScoreEntry = @($latestScore.controlScores) | Where-Object { $_.controlName -eq $controlId } | Select-Object -First 1 if ($null -eq $controlScoreEntry) { $investigateParams.Result = 'The Microsoft Defender for Identity posture recommendation for non-admin Directory Replication permissions was not found in the tenant''s Microsoft Secure Score.' Add-ZtTestResultDetail @investigateParams return } $currentScore = $controlScoreEntry.score $scoreInPercentage = $controlScoreEntry.scoreInPercentage $implementationStatus = $controlScoreEntry.implementationStatus $lastSynced = $controlScoreEntry.lastSynced if ($null -eq $currentScore -or $null -eq $maxScore) { $investigateParams.Result = '⚠️ The Secure Score control returned incomplete score data. Re-run the assessment and investigate if the issue persists.' Add-ZtTestResultDetail @investigateParams return } $customStatus = $null if ($currentScore -eq $maxScore) { $passed = $true $testResultMarkdown = "✅ No non-administrative accounts hold Directory Replication permissions in monitored domains.`n`n%TestResult%" } elseif ($currentScore -lt $maxScore) { $passed = $false $testResultMarkdown = "❌ One or more non-administrative accounts hold Directory Replication permissions and can replicate domain credentials.`n`n%TestResult%" } else { $passed = $false $customStatus = 'Investigate' $testResultMarkdown = "⚠️ The Secure Score control returned inconsistent score data. Current score is greater than maximum score; re-run the assessment and investigate if the issue persists.`n`n%TestResult%" } #endregion Assessment Logic #region Report Generation $scoreDisplay = "$currentScore / $maxScore" $percentageDisplay = if ($null -ne $scoreInPercentage) { "$([math]::Round($scoreInPercentage, 1))%" } else { '—' } $implementationDisplay = if (-not [string]::IsNullOrEmpty($implementationStatus)) { $implementationStatus } else { '—' } $lastSyncedDisplay = if (-not [string]::IsNullOrEmpty($lastSynced)) { Get-FormattedDate -DateString $lastSynced } else { '—' } $controlStateDisplay = $latestState $statusDisplay = if ($passed) { '✅ Pass' } elseif ($customStatus -eq 'Investigate') { '⚠️ Investigate' } else { '❌ Fail' } $defenderLink = 'https://security.microsoft.com/securescore?viewid=actions' $portalLine = '' if (-not $passed -and $customStatus -ne 'Investigate') { $portalLine = "[Defender XDR > Secure Score > Recommendations]($defenderLink)`n`n" } $formatTemplate = @' {0}| Recommendation title | Current score / Maximum score | Score percentage | Implementation status | Last synced | Control state | Status | | :------------------- | ----------------------------: | :--------------- | :-------------------- | :---------- | :------------ | :----- | | {1} | {2} | {3} | {4} | {5} | {6} | {7} | '@ $recommendationTitle = if (-not [string]::IsNullOrWhiteSpace($actionUrl)) { "[$(Get-SafeMarkdown $controlTitle)]($actionUrl)" } else { Get-SafeMarkdown $controlTitle } $mdInfo = $formatTemplate -f $portalLine, $recommendationTitle, $scoreDisplay, $percentageDisplay, $implementationDisplay, $lastSyncedDisplay, $controlStateDisplay, $statusDisplay $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '41011' Title = $title Status = $passed Result = $testResultMarkdown } if ($customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |