PowerShell Setup: Control 2.8 - Access Control and Segregation of Duties
Last Updated: January 2026 Modules Required: Microsoft.Graph, Microsoft.PowerApps.Administration.PowerShell
Prerequisites
Install-Module -Name Microsoft.Graph -Force -Scope CurrentUser
Install-Module -Name Microsoft.PowerApps.Administration.PowerShell -Force -Scope CurrentUser
Automated Scripts
Create Security Groups
<#
.SYNOPSIS
Creates security groups for agent governance roles
.EXAMPLE
.\New-AgentGovernanceGroups.ps1
#>
Write-Host "=== Create Agent Governance Security Groups ===" -ForegroundColor Cyan
Connect-MgGraph -Scopes "Group.ReadWrite.All", "Directory.ReadWrite.All"
$groups = @(
@{DisplayName="SG-Agent-Developers"; Description="Can create and edit agents"; MailNickname="sg-agent-developers"},
@{DisplayName="SG-Agent-Reviewers"; Description="Can review agent submissions"; MailNickname="sg-agent-reviewers"},
@{DisplayName="SG-Agent-Approvers"; Description="Can approve agent deployments"; MailNickname="sg-agent-approvers"},
@{DisplayName="SG-Agent-ReleaseManagers"; Description="Can deploy agents to production"; MailNickname="sg-agent-releasemgrs"},
@{DisplayName="SG-Agent-PlatformAdmins"; Description="Can configure platform settings"; MailNickname="sg-agent-platformadmins"}
)
foreach ($group in $groups) {
$existing = Get-MgGroup -Filter "displayName eq '$($group.DisplayName)'"
if (-not $existing) {
$newGroup = New-MgGroup -DisplayName $group.DisplayName `
-Description $group.Description `
-MailNickname $group.MailNickname `
-MailEnabled:$false `
-SecurityEnabled:$true
Write-Host "[CREATED] $($group.DisplayName)" -ForegroundColor Green
} else {
Write-Host "[EXISTS] $($group.DisplayName)" -ForegroundColor Yellow
}
}
Disconnect-MgGraph
Export Role Membership Report
<#
.SYNOPSIS
Exports membership of agent governance groups
.EXAMPLE
.\Export-AgentGovernanceRoles.ps1
#>
Write-Host "=== Agent Governance Role Membership Report ===" -ForegroundColor Cyan
Connect-MgGraph -Scopes "Group.Read.All", "User.Read.All"
$groupPrefixes = @("SG-Agent-Developers", "SG-Agent-Reviewers", "SG-Agent-Approvers", "SG-Agent-ReleaseManagers", "SG-Agent-PlatformAdmins")
$report = @()
foreach ($prefix in $groupPrefixes) {
$group = Get-MgGroup -Filter "displayName eq '$prefix'"
if ($group) {
$members = Get-MgGroupMember -GroupId $group.Id
foreach ($member in $members) {
$user = Get-MgUser -UserId $member.Id
$report += [PSCustomObject]@{
Group = $prefix
UserPrincipalName = $user.UserPrincipalName
DisplayName = $user.DisplayName
JobTitle = $user.JobTitle
}
}
Write-Host "$prefix : $($members.Count) members" -ForegroundColor Green
}
}
$report | Export-Csv -Path ".\AgentGovernanceRoles_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Write-Host "`nExported to AgentGovernanceRoles_$(Get-Date -Format 'yyyyMMdd').csv" -ForegroundColor Cyan
Disconnect-MgGraph
Segregation of Duties Validation
<#
.SYNOPSIS
Validates no SoD violations exist (user in conflicting roles)
.EXAMPLE
.\Test-SoDCompliance.ps1
#>
Write-Host "=== Segregation of Duties Validation ===" -ForegroundColor Cyan
Connect-MgGraph -Scopes "Group.Read.All", "User.Read.All"
# Define conflicting role pairs
$conflictingPairs = @(
@{Role1="SG-Agent-Developers"; Role2="SG-Agent-Approvers"; Reason="Creator cannot approve own work"},
@{Role1="SG-Agent-Approvers"; Role2="SG-Agent-ReleaseManagers"; Reason="Approver cannot deploy"},
@{Role1="SG-Agent-Developers"; Role2="SG-Agent-ReleaseManagers"; Reason="Creator cannot deploy"}
)
$violations = @()
foreach ($pair in $conflictingPairs) {
$group1 = Get-MgGroup -Filter "displayName eq '$($pair.Role1)'"
$group2 = Get-MgGroup -Filter "displayName eq '$($pair.Role2)'"
if ($group1 -and $group2) {
$members1 = (Get-MgGroupMember -GroupId $group1.Id).Id
$members2 = (Get-MgGroupMember -GroupId $group2.Id).Id
$overlap = $members1 | Where-Object { $_ -in $members2 }
foreach ($userId in $overlap) {
$user = Get-MgUser -UserId $userId
$violations += [PSCustomObject]@{
User = $user.UserPrincipalName
Role1 = $pair.Role1
Role2 = $pair.Role2
Violation = $pair.Reason
}
}
}
}
if ($violations.Count -eq 0) {
Write-Host "[PASS] No SoD violations found" -ForegroundColor Green
} else {
Write-Host "[FAIL] $($violations.Count) SoD violation(s) found:" -ForegroundColor Red
$violations | Format-Table -AutoSize
}
Disconnect-MgGraph
Validation Script
<#
.SYNOPSIS
Validates Control 2.8 - Access Control and Segregation of Duties
.EXAMPLE
.\Validate-Control-2.8.ps1
#>
Write-Host "=== Control 2.8 Validation ===" -ForegroundColor Cyan
Connect-MgGraph -Scopes "Group.Read.All", "RoleManagement.Read.All"
# Check 1: Security groups exist
Write-Host "`n[Check 1] Security Groups" -ForegroundColor Cyan
$requiredGroups = @("SG-Agent-Developers", "SG-Agent-Reviewers", "SG-Agent-Approvers", "SG-Agent-ReleaseManagers", "SG-Agent-PlatformAdmins")
$groupsFound = 0
foreach ($groupName in $requiredGroups) {
$group = Get-MgGroup -Filter "displayName eq '$groupName'"
if ($group) {
Write-Host "[PASS] $groupName exists" -ForegroundColor Green
$groupsFound++
} else {
Write-Host "[FAIL] $groupName not found" -ForegroundColor Red
}
}
# Check 2: PIM configured (check for active assignments)
Write-Host "`n[Check 2] Privileged Identity Management" -ForegroundColor Cyan
Write-Host "[INFO] Verify PIM is configured for admin roles in Entra Admin Center" -ForegroundColor Yellow
# Check 3: Access reviews (manual verification)
Write-Host "`n[Check 3] Access Reviews" -ForegroundColor Cyan
Write-Host "[INFO] Verify quarterly access reviews are scheduled in Identity Governance" -ForegroundColor Yellow
# Check 4: SoD validation
Write-Host "`n[Check 4] Segregation of Duties" -ForegroundColor Cyan
# Run SoD check inline
$conflictingPairs = @(
@{Role1="SG-Agent-Developers"; Role2="SG-Agent-Approvers"},
@{Role1="SG-Agent-Approvers"; Role2="SG-Agent-ReleaseManagers"},
@{Role1="SG-Agent-Developers"; Role2="SG-Agent-ReleaseManagers"}
)
$hasViolations = $false
foreach ($pair in $conflictingPairs) {
$g1 = Get-MgGroup -Filter "displayName eq '$($pair.Role1)'"
$g2 = Get-MgGroup -Filter "displayName eq '$($pair.Role2)'"
if ($g1 -and $g2) {
$m1 = (Get-MgGroupMember -GroupId $g1.Id -ErrorAction SilentlyContinue).Id
$m2 = (Get-MgGroupMember -GroupId $g2.Id -ErrorAction SilentlyContinue).Id
if ($m1 -and $m2) {
$overlap = $m1 | Where-Object { $_ -in $m2 }
if ($overlap) { $hasViolations = $true }
}
}
}
if (-not $hasViolations) {
Write-Host "[PASS] No SoD violations detected" -ForegroundColor Green
} else {
Write-Host "[FAIL] SoD violations detected - run Test-SoDCompliance.ps1 for details" -ForegroundColor Red
}
Write-Host "`n=== Validation Summary ===" -ForegroundColor Cyan
Write-Host "Groups configured: $groupsFound / $($requiredGroups.Count)"
Disconnect-MgGraph
Complete Configuration Script
<#
.SYNOPSIS
Complete access control and SoD configuration for Control 2.8
.DESCRIPTION
Executes end-to-end access control setup including:
- Security group creation
- Role membership audit
- Segregation of duties validation
- Compliance report generation
.PARAMETER OutputPath
Path for output reports
.EXAMPLE
.\Configure-Control-2.8.ps1 -OutputPath ".\AccessControl"
.NOTES
Last Updated: January 2026
Related Control: Control 2.8 - Access Control and Segregation of Duties
#>
param(
[string]$OutputPath = ".\AccessControl-Report"
)
try {
Write-Host "=== Control 2.8: Access Control and Segregation of Duties ===" -ForegroundColor Cyan
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Group.ReadWrite.All", "Directory.ReadWrite.All", "User.Read.All"
# Ensure output directory exists
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
# Define required groups
$requiredGroups = @(
@{DisplayName="SG-Agent-Developers"; Description="Can create and edit agents"; MailNickname="sg-agent-developers"},
@{DisplayName="SG-Agent-Reviewers"; Description="Can review agent submissions"; MailNickname="sg-agent-reviewers"},
@{DisplayName="SG-Agent-Approvers"; Description="Can approve agent deployments"; MailNickname="sg-agent-approvers"},
@{DisplayName="SG-Agent-ReleaseManagers"; Description="Can deploy agents to production"; MailNickname="sg-agent-releasemgrs"},
@{DisplayName="SG-Agent-PlatformAdmins"; Description="Can configure platform settings"; MailNickname="sg-agent-platformadmins"}
)
# Create or verify groups
Write-Host "`n[Step 1] Verifying security groups..." -ForegroundColor Cyan
$groupReport = @()
foreach ($group in $requiredGroups) {
$existing = Get-MgGroup -Filter "displayName eq '$($group.DisplayName)'" -ErrorAction SilentlyContinue
if (-not $existing) {
$newGroup = New-MgGroup -DisplayName $group.DisplayName `
-Description $group.Description `
-MailNickname $group.MailNickname `
-MailEnabled:$false `
-SecurityEnabled:$true
Write-Host " [CREATED] $($group.DisplayName)" -ForegroundColor Green
$groupReport += [PSCustomObject]@{Group=$group.DisplayName; Status="Created"; MemberCount=0}
} else {
$memberCount = (Get-MgGroupMember -GroupId $existing.Id -ErrorAction SilentlyContinue).Count
Write-Host " [EXISTS] $($group.DisplayName) - $memberCount members" -ForegroundColor Yellow
$groupReport += [PSCustomObject]@{Group=$group.DisplayName; Status="Existing"; MemberCount=$memberCount}
}
}
# Export group report
$groupReport | Export-Csv -Path "$OutputPath\SecurityGroups.csv" -NoTypeInformation
# SoD validation
Write-Host "`n[Step 2] Validating Segregation of Duties..." -ForegroundColor Cyan
$conflictingPairs = @(
@{Role1="SG-Agent-Developers"; Role2="SG-Agent-Approvers"; Reason="Creator cannot approve own work"},
@{Role1="SG-Agent-Approvers"; Role2="SG-Agent-ReleaseManagers"; Reason="Approver cannot deploy"},
@{Role1="SG-Agent-Developers"; Role2="SG-Agent-ReleaseManagers"; Reason="Creator cannot deploy"}
)
$violations = @()
foreach ($pair in $conflictingPairs) {
$group1 = Get-MgGroup -Filter "displayName eq '$($pair.Role1)'" -ErrorAction SilentlyContinue
$group2 = Get-MgGroup -Filter "displayName eq '$($pair.Role2)'" -ErrorAction SilentlyContinue
if ($group1 -and $group2) {
$members1 = (Get-MgGroupMember -GroupId $group1.Id -ErrorAction SilentlyContinue).Id
$members2 = (Get-MgGroupMember -GroupId $group2.Id -ErrorAction SilentlyContinue).Id
if ($members1 -and $members2) {
$overlap = $members1 | Where-Object { $_ -in $members2 }
foreach ($userId in $overlap) {
$user = Get-MgUser -UserId $userId -ErrorAction SilentlyContinue
$violations += [PSCustomObject]@{
User = $user.UserPrincipalName
Role1 = $pair.Role1
Role2 = $pair.Role2
Violation = $pair.Reason
}
}
}
}
}
if ($violations.Count -eq 0) {
Write-Host " [PASS] No SoD violations found" -ForegroundColor Green
} else {
Write-Host " [FAIL] $($violations.Count) SoD violation(s) found" -ForegroundColor Red
$violations | Export-Csv -Path "$OutputPath\SoD-Violations.csv" -NoTypeInformation
}
# Summary
Write-Host "`n=== Summary ===" -ForegroundColor Cyan
Write-Host "Security Groups: $($groupReport.Count)"
Write-Host "SoD Violations: $($violations.Count)"
Write-Host "Report Path: $OutputPath"
Write-Host "`n[PASS] Control 2.8 configuration completed successfully" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "[INFO] Stack trace: $($_.ScriptStackTrace)" -ForegroundColor Yellow
exit 1
}
finally {
# Cleanup connections
Disconnect-MgGraph -ErrorAction SilentlyContinue
}
Back to Control 2.8 | Portal Walkthrough | Verification Testing | Troubleshooting