Identity Governance and Administration (IGA) is a framework that provides organizations with the ability to manage user identities, enforce access policies, ensure compliance, and reduce security risks through automated identity lifecycle management.
🎯 Identity Governance Framework
Core Principles
Identity Lifecycle Management
- Automated provisioning and deprovisioning
- Role-based access control (RBAC)
- Attribute-based access control (ABAC)
- Just-in-time (JIT) access provisioning
Access Governance
- Access certification and reviews
- Segregation of duties (SoD) enforcement
- Privileged access management (PAM)
- Risk-based access decisions
Compliance and Audit
- Regulatory compliance reporting
- Access audit trails
- Policy violation detection
- Continuous compliance monitoring
🏗️ Microsoft Identity Governance Architecture
Entra ID Governance
<#
.SYNOPSIS
Microsoft Entra ID Governance management and automation
.DESCRIPTION
Comprehensive functions for managing Entra ID Governance features including
access reviews, entitlement management, and privileged identity management
#>
function New-AccessReviewCampaign
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$CampaignName,
[Parameter(Mandatory = $true)]
[string]$Description,
[Parameter(Mandatory = $true)]
[ValidateSet("Groups", "Applications", "Roles", "Teams")]
[string]$ReviewType,
[Parameter()]
[int]$DurationInDays = 14,
[Parameter()]
[string[]]$Reviewers = @(),
[Parameter()]
[ValidateSet("Weekly", "Monthly", "Quarterly", "SemiAnnually", "Annually")]
[string]$Frequency = "Quarterly",
[Parameter()]
[switch]$AutoApplyRecommendations,
[Parameter()]
[switch]$RemoveAccessOnNonResponse
)
try
{
Write-Host "Creating Access Review Campaign: $CampaignName" -ForegroundColor Green
# Connect to Microsoft Graph if not already connected
try
{
$Context = Get-MgContext
if (-not $Context)
{
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow
Connect-MgGraph -Scopes "AccessReview.ReadWrite.All", "Directory.Read.All"
}
}
catch
{
throw "Failed to connect to Microsoft Graph: $($_.Exception.Message)"
}
# Build access review configuration
$ReviewConfig = @{
displayName = $CampaignName
descriptionForAdmins = $Description
descriptionForReviewers = "Please review access and confirm if users still require access"
scope = @{}
reviewers = @()
settings = @{
mailNotificationsEnabled = $true
reminderNotificationsEnabled = $true
defaultDecision = "None"
defaultDecisionEnabled = $false
instanceDurationInDays = $DurationInDays
autoApplyDecisionsEnabled = $AutoApplyRecommendations.IsPresent
recommendationsEnabled = $true
justificationRequiredOnApproval = $true
applyActions = @()
}
recurrence = @{
pattern = @{
type = switch ($Frequency) {
"Weekly" { "weekly" }
"Monthly" { "absoluteMonthly" }
"Quarterly" { "absoluteMonthly" }
"SemiAnnually" { "absoluteMonthly" }
"Annually" { "absoluteYearly" }
}
interval = switch ($Frequency) {
"Weekly" { 1 }
"Monthly" { 1 }
"Quarterly" { 3 }
"SemiAnnually" { 6 }
"Annually" { 1 }
}
}
range = @{
type = "noEnd"
startDate = (Get-Date).ToString("yyyy-MM-dd")
}
}
}
# Configure scope based on review type
switch ($ReviewType)
{
"Groups" {
$ReviewConfig.scope = @{
'@odata.type' = "#microsoft.graph.accessReviewQueryScope"
query = "/groups?$filter=(groupTypes/any(c:c eq 'Unified') or securityEnabled eq true)"
queryType = "MicrosoftGraph"
}
}
"Applications" {
$ReviewConfig.scope = @{
'@odata.type' = "#microsoft.graph.accessReviewQueryScope"
query = "/servicePrincipals"
queryType = "MicrosoftGraph"
}
}
"Roles" {
$ReviewConfig.scope = @{
'@odata.type' = "#microsoft.graph.accessReviewQueryScope"
query = "/roleManagement/directory/roleAssignments"
queryType = "MicrosoftGraph"
}
}
"Teams" {
$ReviewConfig.scope = @{
'@odata.type' = "#microsoft.graph.accessReviewQueryScope"
query = "/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')"
queryType = "MicrosoftGraph"
}
}
}
# Configure reviewers
if ($Reviewers.Count -gt 0)
{
$ReviewConfig.reviewers = $Reviewers | ForEach-Object {
@{
query = "/users/$_"
queryType = "MicrosoftGraph"
}
}
}
else
{
# Default to resource owners as reviewers
$ReviewConfig.reviewers = @(
@{
query = "./manager"
queryType = "MicrosoftGraph"
queryRoot = "decisions"
}
)
}
# Configure automatic actions
if ($RemoveAccessOnNonResponse)
{
$ReviewConfig.settings.applyActions += @{
'@odata.type' = "#microsoft.graph.removeAccessApplyAction"
}
}
# Create the access review
$AccessReview = New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $ReviewConfig
Write-Host "✓ Access Review Campaign created successfully" -ForegroundColor Green
Write-Host " Campaign ID: $($AccessReview.Id)" -ForegroundColor Cyan
Write-Host " Review Type: $ReviewType" -ForegroundColor Cyan
Write-Host " Duration: $DurationInDays days" -ForegroundColor Cyan
Write-Host " Frequency: $Frequency" -ForegroundColor Cyan
return [PSCustomObject]@{
CampaignId = $AccessReview.Id
CampaignName = $CampaignName
ReviewType = $ReviewType
Status = "Created"
NextReviewDate = $AccessReview.Instances[0].StartDateTime
Configuration = $ReviewConfig
}
}
catch
{
Write-Error "Failed to create access review campaign: $($_.Exception.Message)"
throw
}
}
function Get-AccessReviewStatus
{
[CmdletBinding()]
param(
[Parameter()]
[string]$CampaignId,
[Parameter()]
[switch]$IncludeDecisions,
[Parameter()]
[switch]$ShowMetrics
)
try
{
Write-Host "Retrieving access review status..." -ForegroundColor Green
# Get all access review definitions if no specific campaign ID provided
if (-not $CampaignId)
{
$AccessReviews = Get-MgIdentityGovernanceAccessReviewDefinition -All
$ReviewSummary = $AccessReviews | ForEach-Object {
$Review = $_
$Instances = Get-MgIdentityGovernanceAccessReviewDefinitionInstance -AccessReviewScheduleDefinitionId $Review.Id
[PSCustomObject]@{
CampaignId = $Review.Id
CampaignName = $Review.DisplayName
Status = $Review.Status
ReviewType = $Review.Scope.'@odata.type'
ActiveInstances = ($Instances | Where-Object Status -eq "InProgress").Count
CompletedInstances = ($Instances | Where-Object Status -eq "Completed").Count
NextReviewDate = ($Instances | Where-Object Status -eq "NotStarted" | Sort-Object StartDateTime | Select-Object -First 1).StartDateTime
}
}
Write-Host "Access Review Campaigns Summary:" -ForegroundColor Cyan
$ReviewSummary | Format-Table -AutoSize
return $ReviewSummary
}
# Get specific campaign details
$Campaign = Get-MgIdentityGovernanceAccessReviewDefinition -AccessReviewScheduleDefinitionId $CampaignId
$Instances = Get-MgIdentityGovernanceAccessReviewDefinitionInstance -AccessReviewScheduleDefinitionId $CampaignId
$CampaignStatus = [PSCustomObject]@{
CampaignId = $Campaign.Id
CampaignName = $Campaign.DisplayName
Description = $Campaign.DescriptionForAdmins
Status = $Campaign.Status
CreatedDate = $Campaign.CreatedDateTime
LastModified = $Campaign.LastModifiedDateTime
TotalInstances = $Instances.Count
ActiveInstances = ($Instances | Where-Object Status -eq "InProgress").Count
CompletedInstances = ($Instances | Where-Object Status -eq "Completed").Count
PendingInstances = ($Instances | Where-Object Status -eq "NotStarted").Count
}
Write-Host "Campaign Status:" -ForegroundColor Cyan
$CampaignStatus | Format-List
if ($ShowMetrics)
{
Write-Host "Campaign Metrics:" -ForegroundColor Yellow
foreach ($Instance in $Instances | Where-Object Status -eq "Completed")
{
$Decisions = Get-MgIdentityGovernanceAccessReviewDefinitionInstanceDecision -AccessReviewScheduleDefinitionId $CampaignId -AccessReviewInstanceId $Instance.Id
$InstanceMetrics = [PSCustomObject]@{
InstanceId = $Instance.Id
ReviewPeriod = "$($Instance.StartDateTime) to $($Instance.EndDateTime)"
TotalDecisions = $Decisions.Count
ApprovedCount = ($Decisions | Where-Object Decision -eq "Approve").Count
DeniedCount = ($Decisions | Where-Object Decision -eq "Deny").Count
NotReviewedCount = ($Decisions | Where-Object Decision -eq "NotReviewed").Count
CompletionRate = [math]::Round((($Decisions | Where-Object {$_.Decision -ne "NotReviewed"}).Count / $Decisions.Count) * 100, 2)
}
$InstanceMetrics | Format-List
}
}
if ($IncludeDecisions)
{
Write-Host "Recent Decisions:" -ForegroundColor Yellow
$LatestInstance = $Instances | Sort-Object StartDateTime -Descending | Select-Object -First 1
if ($LatestInstance)
{
$RecentDecisions = Get-MgIdentityGovernanceAccessReviewDefinitionInstanceDecision -AccessReviewScheduleDefinitionId $CampaignId -AccessReviewInstanceId $LatestInstance.Id | Select-Object -First 20
$RecentDecisions | ForEach-Object {
[PSCustomObject]@{
ResourceName = $_.Resource.DisplayName
PrincipalName = $_.Principal.DisplayName
Decision = $_.Decision
Justification = $_.Justification
ReviewedBy = $_.ReviewedBy.DisplayName
ReviewedDate = $_.ReviewedDateTime
}
} | Format-Table -AutoSize
}
}
return $CampaignStatus
}
catch
{
Write-Error "Failed to retrieve access review status: $($_.Exception.Message)"
throw
}
}
Entitlement Management
function New-AccessPackage
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$PackageName,
[Parameter(Mandatory = $true)]
[string]$Description,
[Parameter(Mandatory = $true)]
[string]$CatalogId,
[Parameter()]
[string[]]$ResourceRoles = @(),
[Parameter()]
[hashtable]$RequestorSettings = @{},
[Parameter()]
[hashtable]$ApprovalSettings = @{},
[Parameter()]
[int]$AccessDurationDays = 90
)
try
{
Write-Host "Creating Access Package: $PackageName" -ForegroundColor Green
# Build access package configuration
$AccessPackageConfig = @{
displayName = $PackageName
description = $Description
catalogId = $CatalogId
isHidden = $false
accessPackageResourceRoleScopes = @()
}
# Add resource roles if specified
foreach ($ResourceRole in $ResourceRoles)
{
# This would need to be expanded based on specific resource types
$AccessPackageConfig.accessPackageResourceRoleScopes += @{
accessPackageResourceRole = @{
id = $ResourceRole
}
accessPackageResourceScope = @{
id = "root"
}
}
}
# Create the access package
$AccessPackage = New-MgEntitlementManagementAccessPackage -BodyParameter $AccessPackageConfig
# Create assignment policy
$PolicyConfig = @{
displayName = "$PackageName - Default Policy"
description = "Default assignment policy for $PackageName"
accessPackageId = $AccessPackage.Id
expiration = @{
endDateTime = $null
duration = "P$($AccessDurationDays)D"
type = "afterDuration"
}
requestorSettings = @{
scopeType = "AllExistingDirectoryMemberUsers"
acceptRequests = $true
allowedRequestors = @()
}
requestApprovalSettings = @{
isApprovalRequired = $true
isApprovalRequiredForExtension = $false
isRequestorJustificationRequired = $true
approvalMode = "SingleStage"
approvalStages = @(
@{
approvalStageTimeOutInDays = 14
isApproverJustificationRequired = $true
isEscalationEnabled = $false
primaryApprovers = @()
}
)
}
}
# Apply custom settings if provided
if ($RequestorSettings.Count -gt 0)
{
$PolicyConfig.requestorSettings = $RequestorSettings
}
if ($ApprovalSettings.Count -gt 0)
{
$PolicyConfig.requestApprovalSettings = $ApprovalSettings
}
$AssignmentPolicy = New-MgEntitlementManagementAccessPackageAssignmentPolicy -BodyParameter $PolicyConfig
Write-Host "✓ Access Package created successfully" -ForegroundColor Green
Write-Host " Package ID: $($AccessPackage.Id)" -ForegroundColor Cyan
Write-Host " Policy ID: $($AssignmentPolicy.Id)" -ForegroundColor Cyan
Write-Host " Access Duration: $AccessDurationDays days" -ForegroundColor Cyan
return [PSCustomObject]@{
AccessPackageId = $AccessPackage.Id
PackageName = $PackageName
CatalogId = $CatalogId
PolicyId = $AssignmentPolicy.Id
Status = "Created"
}
}
catch
{
Write-Error "Failed to create access package: $($_.Exception.Message)"
throw
}
}
function Get-AccessPackageRequests
{
[CmdletBinding()]
param(
[Parameter()]
[string]$AccessPackageId,
[Parameter()]
[ValidateSet("Pending", "Approved", "Denied", "Delivered", "All")]
[string]$Status = "All",
[Parameter()]
[int]$DaysBack = 30
)
try
{
Write-Host "Retrieving access package requests..." -ForegroundColor Green
$StartDate = (Get-Date).AddDays(-$DaysBack)
# Get all assignment requests
$AllRequests = Get-MgEntitlementManagementAccessPackageAssignmentRequest -All
# Filter by access package if specified
if ($AccessPackageId)
{
$AllRequests = $AllRequests | Where-Object { $_.AccessPackage.Id -eq $AccessPackageId }
}
# Filter by date range
$AllRequests = $AllRequests | Where-Object { [DateTime]$_.CreatedDateTime -ge $StartDate }
# Filter by status if not "All"
if ($Status -ne "All")
{
$AllRequests = $AllRequests | Where-Object { $_.State -eq $Status }
}
$RequestSummary = $AllRequests | ForEach-Object {
[PSCustomObject]@{
RequestId = $_.Id
RequestedBy = $_.Requestor.DisplayName
RequestedByUPN = $_.Requestor.UserPrincipalName
AccessPackage = $_.AccessPackage.DisplayName
RequestType = $_.RequestType
Status = $_.State
RequestDate = $_.CreatedDateTime
CompletedDate = $_.CompletedDateTime
Justification = $_.Answers | Where-Object { $_.DisplayValue } | Select-Object -First 1 -ExpandProperty DisplayValue
}
}
Write-Host "Access Package Requests Summary:" -ForegroundColor Cyan
Write-Host "Total Requests: $($RequestSummary.Count)" -ForegroundColor White
if ($Status -eq "All")
{
$StatusGroups = $RequestSummary | Group-Object Status
$StatusGroups | ForEach-Object {
Write-Host "$($_.Name): $($_.Count)" -ForegroundColor Yellow
}
}
$RequestSummary | Format-Table -AutoSize
return $RequestSummary
}
catch
{
Write-Error "Failed to retrieve access package requests: $($_.Exception.Message)"
throw
}
}
🔒 Privileged Identity Management (PIM)
PIM Role Management
function Enable-PIMRole
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$RoleName,
[Parameter(Mandatory = $true)]
[string]$PrincipalId,
[Parameter()]
[int]$DurationHours = 8,
[Parameter(Mandatory = $true)]
[string]$Justification,
[Parameter()]
[string]$TicketNumber
)
try
{
Write-Host "Activating PIM role: $RoleName for $PrincipalId" -ForegroundColor Green
# Get role definition
$RoleDefinition = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq '$RoleName'"
if (-not $RoleDefinition)
{
throw "Role '$RoleName' not found"
}
# Create activation request
$ActivationRequest = @{
action = "selfActivate"
principalId = $PrincipalId
roleDefinitionId = $RoleDefinition.Id
directoryScopeId = "/"
justification = $Justification
scheduleInfo = @{
startDateTime = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
expiration = @{
type = "afterDuration"
duration = "PT$($DurationHours)H"
}
}
}
# Add ticket number to justification if provided
if ($TicketNumber)
{
$ActivationRequest.justification += " (Ticket: $TicketNumber)"
}
$RoleAssignmentRequest = New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $ActivationRequest
Write-Host "✓ PIM role activation requested" -ForegroundColor Green
Write-Host " Request ID: $($RoleAssignmentRequest.Id)" -ForegroundColor Cyan
Write-Host " Role: $RoleName" -ForegroundColor Cyan
Write-Host " Duration: $DurationHours hours" -ForegroundColor Cyan
Write-Host " Status: $($RoleAssignmentRequest.Status)" -ForegroundColor Cyan
return [PSCustomObject]@{
RequestId = $RoleAssignmentRequest.Id
RoleName = $RoleName
PrincipalId = $PrincipalId
Duration = $DurationHours
Status = $RoleAssignmentRequest.Status
RequestedAt = Get-Date
}
}
catch
{
Write-Error "Failed to activate PIM role: $($_.Exception.Message)"
throw
}
}
function Get-PIMRoleAssignments
{
[CmdletBinding()]
param(
[Parameter()]
[string]$PrincipalId,
[Parameter()]
[ValidateSet("Active", "Eligible", "All")]
[string]$AssignmentType = "All",
[Parameter()]
[switch]$IncludeExpiredAssignments
)
try
{
Write-Host "Retrieving PIM role assignments..." -ForegroundColor Green
# Get role assignments
if ($AssignmentType -eq "All" -or $AssignmentType -eq "Active")
{
$ActiveAssignments = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -All
if ($PrincipalId)
{
$ActiveAssignments = $ActiveAssignments | Where-Object PrincipalId -eq $PrincipalId
}
$ActiveResults = $ActiveAssignments | ForEach-Object {
$RoleDefinition = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $_.RoleDefinitionId
[PSCustomObject]@{
AssignmentType = "Active"
RoleName = $RoleDefinition.DisplayName
PrincipalId = $_.PrincipalId
StartDateTime = $_.StartDateTime
EndDateTime = $_.EndDateTime
AssignmentId = $_.Id
Status = if ($_.EndDateTime -and [DateTime]$_.EndDateTime -lt (Get-Date)) { "Expired" } else { "Active" }
}
}
}
if ($AssignmentType -eq "All" -or $AssignmentType -eq "Eligible")
{
$EligibleAssignments = Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -All
if ($PrincipalId)
{
$EligibleAssignments = $EligibleAssignments | Where-Object PrincipalId -eq $PrincipalId
}
$EligibleResults = $EligibleAssignments | ForEach-Object {
$RoleDefinition = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $_.RoleDefinitionId
[PSCustomObject]@{
AssignmentType = "Eligible"
RoleName = $RoleDefinition.DisplayName
PrincipalId = $_.PrincipalId
StartDateTime = $_.StartDateTime
EndDateTime = $_.EndDateTime
AssignmentId = $_.Id
Status = if ($_.EndDateTime -and [DateTime]$_.EndDateTime -lt (Get-Date)) { "Expired" } else { "Eligible" }
}
}
}
# Combine results
$AllAssignments = @()
if ($ActiveResults) { $AllAssignments += $ActiveResults }
if ($EligibleResults) { $AllAssignments += $EligibleResults }
# Filter expired assignments if not requested
if (-not $IncludeExpiredAssignments)
{
$AllAssignments = $AllAssignments | Where-Object Status -ne "Expired"
}
Write-Host "PIM Role Assignments Summary:" -ForegroundColor Cyan
$AssignmentGroups = $AllAssignments | Group-Object AssignmentType
$AssignmentGroups | ForEach-Object {
Write-Host "$($_.Name): $($_.Count)" -ForegroundColor Yellow
}
$AllAssignments | Sort-Object RoleName, AssignmentType | Format-Table -AutoSize
return $AllAssignments
}
catch
{
Write-Error "Failed to retrieve PIM role assignments: $($_.Exception.Message)"
throw
}
}
📊 Compliance and Reporting
Identity Compliance Dashboard
function Get-IdentityComplianceDashboard
{
[CmdletBinding()]
param(
[Parameter()]
[int]$DaysBack = 30,
[Parameter()]
[switch]$IncludeDetails,
[Parameter()]
[string]$ExportPath
)
try
{
Write-Host "Generating Identity Compliance Dashboard..." -ForegroundColor Green
$ComplianceData = [PSCustomObject]@{
GeneratedDate = Get-Date
ReportingPeriod = "$DaysBack days"
UserAccounts = @{}
PrivilegedAccounts = @{}
AccessReviews = @{}
AccessPackages = @{}
PIMActivations = @{}
ComplianceScore = 0
RiskFactors = @()
Recommendations = @()
}
# User account statistics
Write-Host "Analyzing user accounts..." -ForegroundColor Cyan
$AllUsers = Get-MgUser -All -Property "signInActivity,createdDateTime,accountEnabled,lastPasswordChangeDateTime,userPrincipalName,displayName"
$ComplianceData.UserAccounts = @{
Total = $AllUsers.Count
Enabled = ($AllUsers | Where-Object AccountEnabled -eq $true).Count
Disabled = ($AllUsers | Where-Object AccountEnabled -eq $false).Count
RecentlyCreated = ($AllUsers | Where-Object { [DateTime]$_.CreatedDateTime -ge (Get-Date).AddDays(-$DaysBack) }).Count
InactiveUsers = ($AllUsers | Where-Object {
$_.SignInActivity.LastSignInDateTime -and
[DateTime]$_.SignInActivity.LastSignInDateTime -lt (Get-Date).AddDays(-90)
}).Count
NeverSignedIn = ($AllUsers | Where-Object { -not $_.SignInActivity.LastSignInDateTime }).Count
}
# Privileged account analysis
Write-Host "Analyzing privileged accounts..." -ForegroundColor Cyan
$PrivilegedRoles = @(
"Global Administrator",
"Privileged Role Administrator",
"User Administrator",
"Security Administrator",
"Exchange Administrator"
)
$PrivilegedAssignments = @()
foreach ($Role in $PrivilegedRoles)
{
$RoleDefinition = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq '$Role'"
if ($RoleDefinition)
{
$Assignments = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -Filter "roleDefinitionId eq '$($RoleDefinition.Id)'"
$PrivilegedAssignments += $Assignments | ForEach-Object {
[PSCustomObject]@{
RoleName = $Role
PrincipalId = $_.PrincipalId
AssignmentType = "Active"
StartDateTime = $_.StartDateTime
EndDateTime = $_.EndDateTime
}
}
}
}
$ComplianceData.PrivilegedAccounts = @{
TotalPrivilegedUsers = ($PrivilegedAssignments | Select-Object -Unique PrincipalId).Count
RoleAssignments = $PrivilegedAssignments.Count
RoleDistribution = $PrivilegedAssignments | Group-Object RoleName | ForEach-Object {
[PSCustomObject]@{
RoleName = $_.Name
AssignmentCount = $_.Count
}
}
}
# Access review analysis
Write-Host "Analyzing access reviews..." -ForegroundColor Cyan
try
{
$AccessReviewDefinitions = Get-MgIdentityGovernanceAccessReviewDefinition -All
$RecentReviews = @()
foreach ($Review in $AccessReviewDefinitions)
{
$Instances = Get-MgIdentityGovernanceAccessReviewDefinitionInstance -AccessReviewScheduleDefinitionId $Review.Id
$RecentInstances = $Instances | Where-Object {
[DateTime]$_.StartDateTime -ge (Get-Date).AddDays(-$DaysBack)
}
foreach ($Instance in $RecentInstances)
{
$Decisions = Get-MgIdentityGovernanceAccessReviewDefinitionInstanceDecision -AccessReviewScheduleDefinitionId $Review.Id -AccessReviewInstanceId $Instance.Id
$RecentReviews += [PSCustomObject]@{
ReviewName = $Review.DisplayName
InstanceId = $Instance.Id
Status = $Instance.Status
StartDate = $Instance.StartDateTime
EndDate = $Instance.EndDateTime
TotalDecisions = $Decisions.Count
CompletedDecisions = ($Decisions | Where-Object Decision -ne "NotReviewed").Count
ApprovedCount = ($Decisions | Where-Object Decision -eq "Approve").Count
DeniedCount = ($Decisions | Where-Object Decision -eq "Deny").Count
}
}
}
$ComplianceData.AccessReviews = @{
ActiveCampaigns = $AccessReviewDefinitions.Count
RecentReviews = $RecentReviews.Count
CompletionRate = if ($RecentReviews.Count -gt 0)
{
[math]::Round((($RecentReviews | Where-Object Status -eq "Completed").Count / $RecentReviews.Count) * 100, 2)
}
else
{
0
}
DecisionStats = if ($RecentReviews.Count -gt 0)
{
@{
TotalDecisions = ($RecentReviews | Measure-Object TotalDecisions -Sum).Sum
CompletedDecisions = ($RecentReviews | Measure-Object CompletedDecisions -Sum).Sum
ApprovedCount = ($RecentReviews | Measure-Object ApprovedCount -Sum).Sum
DeniedCount = ($RecentReviews | Measure-Object DeniedCount -Sum).Sum
}
}
else
{
@{
TotalDecisions = 0
CompletedDecisions = 0
ApprovedCount = 0
DeniedCount = 0
}
}
}
}
catch
{
$ComplianceData.AccessReviews = @{ Error = "Unable to retrieve access review data" }
}
# Calculate compliance score
$ScoreFactors = @{
InactiveUserRatio = if ($ComplianceData.UserAccounts.Total -gt 0) {
1 - ($ComplianceData.UserAccounts.InactiveUsers / $ComplianceData.UserAccounts.Total)
} else { 1 }
AccessReviewCompletion = $ComplianceData.AccessReviews.CompletionRate / 100
PrivilegedAccountRatio = if ($ComplianceData.UserAccounts.Enabled -gt 0)
{
1 - ([math]::Min($ComplianceData.PrivilegedAccounts.TotalPrivilegedUsers / $ComplianceData.UserAccounts.Enabled, 0.1) / 0.1)
} else { 1 }
}
$ComplianceData.ComplianceScore = [math]::Round((
($ScoreFactors.InactiveUserRatio * 0.4) +
($ScoreFactors.AccessReviewCompletion * 0.4) +
($ScoreFactors.PrivilegedAccountRatio * 0.2)
) * 100, 2)
# Generate risk factors and recommendations
if ($ComplianceData.UserAccounts.InactiveUsers -gt ($ComplianceData.UserAccounts.Total * 0.1))
{
$ComplianceData.RiskFactors += "High number of inactive users ($($ComplianceData.UserAccounts.InactiveUsers))"
$ComplianceData.Recommendations += "Review and disable inactive user accounts"
}
if ($ComplianceData.UserAccounts.NeverSignedIn -gt 0)
{
$ComplianceData.RiskFactors += "$($ComplianceData.UserAccounts.NeverSignedIn) users have never signed in"
$ComplianceData.Recommendations += "Review users who have never signed in and consider account cleanup"
}
if ($ComplianceData.AccessReviews.CompletionRate -lt 80)
{
$ComplianceData.RiskFactors += "Low access review completion rate ($($ComplianceData.AccessReviews.CompletionRate)%)"
$ComplianceData.Recommendations += "Improve access review campaign communication and follow-up"
}
# Display dashboard
Write-Host "`nIdentity Governance Compliance Dashboard" -ForegroundColor Green
Write-Host "=" * 50 -ForegroundColor Green
Write-Host "Report Date: $($ComplianceData.GeneratedDate)" -ForegroundColor White
Write-Host "Reporting Period: $($ComplianceData.ReportingPeriod)" -ForegroundColor White
Write-Host "Overall Compliance Score: $($ComplianceData.ComplianceScore)%" -ForegroundColor $(
if ($ComplianceData.ComplianceScore -ge 90) { "Green" }
elseif ($ComplianceData.ComplianceScore -ge 75) { "Yellow" }
else { "Red" }
)
Write-Host "`nUser Account Statistics:" -ForegroundColor Cyan
Write-Host " Total Users: $($ComplianceData.UserAccounts.Total)" -ForegroundColor White
Write-Host " Enabled: $($ComplianceData.UserAccounts.Enabled)" -ForegroundColor Green
Write-Host " Disabled: $($ComplianceData.UserAccounts.Disabled)" -ForegroundColor Yellow
Write-Host " Inactive (90+ days): $($ComplianceData.UserAccounts.InactiveUsers)" -ForegroundColor Red
Write-Host " Never Signed In: $($ComplianceData.UserAccounts.NeverSignedIn)" -ForegroundColor Red
Write-Host "`nPrivileged Account Analysis:" -ForegroundColor Cyan
Write-Host " Privileged Users: $($ComplianceData.PrivilegedAccounts.TotalPrivilegedUsers)" -ForegroundColor White
Write-Host " Role Assignments: $($ComplianceData.PrivilegedAccounts.RoleAssignments)" -ForegroundColor White
if ($ComplianceData.AccessReviews.Error)
{
Write-Host "`nAccess Reviews: $($ComplianceData.AccessReviews.Error)" -ForegroundColor Red
}
else
{
Write-Host "`nAccess Review Status:" -ForegroundColor Cyan
Write-Host " Active Campaigns: $($ComplianceData.AccessReviews.ActiveCampaigns)" -ForegroundColor White
Write-Host " Recent Reviews: $($ComplianceData.AccessReviews.RecentReviews)" -ForegroundColor White
Write-Host " Completion Rate: $($ComplianceData.AccessReviews.CompletionRate)%" -ForegroundColor White
}
if ($ComplianceData.RiskFactors.Count -gt 0)
{
Write-Host "`nRisk Factors:" -ForegroundColor Red
$ComplianceData.RiskFactors | ForEach-Object {
Write-Host " ⚠ $_" -ForegroundColor Yellow
}
}
if ($ComplianceData.Recommendations.Count -gt 0)
{
Write-Host "`nRecommendations:" -ForegroundColor Yellow
$ComplianceData.Recommendations | ForEach-Object {
Write-Host " • $_" -ForegroundColor White
}
}
# Export if requested
if ($ExportPath)
{
$ComplianceData | ConvertTo-Json -Depth 5 | Out-File -FilePath $ExportPath
Write-Host "`nDashboard data exported to: $ExportPath" -ForegroundColor Green
}
return $ComplianceData
}
catch
{
Write-Error "Failed to generate compliance dashboard: $($_.Exception.Message)"
throw
}
}
# Example usage and best practices documentation
Write-Host @"
IDENTITY GOVERNANCE IMPLEMENTATION GUIDE
Access Reviews Best Practices:
• Implement quarterly access reviews for all security groups
• Require manager approval for access extensions
• Enable automatic access removal for non-responsive reviews
• Use risk-based scoping for high-privilege roles
Entitlement Management:
• Create access packages for common role-based access patterns
• Implement approval workflows for sensitive resources
• Set appropriate access duration based on business needs
• Regular review of access package usage and effectiveness
Privileged Identity Management:
• Enable PIM for all administrative roles
• Require justification and approval for role activation
• Set maximum activation duration based on role requirements
• Monitor PIM activation patterns for anomalies
Compliance Monitoring:
• Generate monthly compliance dashboards
• Track access review completion rates
• Monitor inactive and unused accounts
• Regular audit of privileged role assignments
Automation Strategies:
• Automated provisioning based on HR system changes
• Dynamic group membership for role-based access
• Automated access certification reminders
• Risk-based conditional access policies
"@ -ForegroundColor Greeny Governance"
description: "Documentation for Identity Governance"
author: "Joseph Streeter"
ms.date: "2025-07-18"
ms.topic: "article"
---
## Identity Governance
This is a placeholder for Identity Governance content.
## Overview
Content will be added here soon.
## Topics
Add topics here.