Skip to content

Instantly share code, notes, and snippets.

@jschlackman
Last active October 17, 2023 21:26
Show Gist options
  • Select an option

  • Save jschlackman/dc72fb422e548ac5dfb94fde67861b55 to your computer and use it in GitHub Desktop.

Select an option

Save jschlackman/dc72fb422e548ac5dfb94fde67861b55 to your computer and use it in GitHub Desktop.
# Name: Get-DirectoryAccessDetails.ps1
# Author: James Schlackman
# Last Modified: Oct 17 2023
#
# Audits all enabled users in specified on-prem AD OUs and combines with it with enabled users from Entra ID.
# Outputs a combined list showing which accounts are federated/on-prem only/cloud only along with details of
# the most recent date they logged in (either on-prem or cloud, whichever is later), when they last changed
# their password, when the account was created, and which AD admin and Entra roles they are assigned (if any).
#
# Last sign in date for Entra requires an Entra ID Premium license.
#
# NOTE: Entra ID only began storing account creation dates in June 2018. CreatedDateTime will be blank
# for accounts created before that time.
#
# Source: https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.users/update-mguser
#Requires -Modules ActiveDirectory, Microsoft.Graph.Authentication, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Users
Param(
# Export file name
[Parameter()] [String]$OutputPath = "$((Get-Date).ToString("yyMMdd")) Directory Access Details.csv",
# One or more OUs to check. Must include all OUs synced by Entra Connect for identity type field to be accurate. Defaults to root of current domain.
[Parameter()] [String[]]$CheckOUs = (Get-ADDomain).DistinguishedName,
# PIM groups to be included in audit. Must be specified manually until Graph has a way of retrieving a list programatically.
[Parameter()] [String[]]$EntraPimGroups
)
# Init empty hash table
$AllUsers = @{}
# Function to convert UTC datetimes returned by Graph to local time
$LocalTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Eastern Standard Time")
function LocalTime() {
param(
$utcDateTime
)
If ([bool]$utcDateTime) {
[System.TimeZoneInfo]::ConvertTimeFromUtc($utcDateTime, $LocalTimeZone)
}
}
# Connect to Graph with required scopes
Connect-Graph -Scopes User.Read.All,AuditLog.Read.All,RoleManagement.Read.All
Write-Host "`nLoading AD admin group users..." -ForegroundColor Cyan
# Get all privileged groups with their members
$ADAdminAudit = Get-ADGroup -Filter 'AdminCount -eq 1' | Select 'name',@{Name='members';Expression={(Get-ADGroupMember -Identity $_ -Recursive).sAMAccountName}}
If ([bool]$ADAdminAudit) {
Write-Host "Complete." -ForegroundColor Green
}
Write-Host "`nLoading AD user details..." -ForegroundColor Cyan
$ADLoadProperties = 'passwordLastSet','mail','description','lastLogonTimestamp','msDS-UserPasswordExpiryTimeComputed','title','department','whenCreated'
# Load details for each specified OU
$ADUsers = $CheckOUs | ForEach-Object {
Write-Host $_
# Get user details
Get-ADUser -SearchBase $_ -SearchScope Subtree -Filter {(Enabled -eq $true ) -and (passwordLastSet -gt 0)} -Properties $ADLoadProperties | ForEach-Object {
[PSCustomObject]@{
UserPrincipalName = If($_.UserPrincipalName -ne $null){$_.UserPrincipalName} Else {$_.sAMAccountName}
Name = $_.Name
Mail = $_.Mail
SharedMailbox = ''
IdentityType = 'On-Premise'
Description = If ($_.Title -ne $null) {$_.Title} Else {$_.Description}
Department = $_.Department
Created = $_.whenCreated
'Password Last Set' = $_.passwordLastSet
LastSignIn = If ($_.lastLogonTimestamp -gt 0) {[datetime]::FromFileTime($_.lastLogonTimestamp)} Else {''}
'AD Admin Groups' = (($ADAdminAudit | Where-Object -Property Members -Contains $_.sAMAccountName).name) -join ', '
'Entra Roles' = ''
OnPremisesDistinguishedName =$_.DistinguishedName
}
}
}
If ([bool]$ADUsers) {
Write-Host "Complete." -ForegroundColor Green
}
# Add AD users to hash table
$ADUsers | ForEach-Object {
$AllUsers.Add($_.UserPrincipalName, $_)
}
# Get all active AAD roles and the users assigned to them
Write-Host "`nRetrieving Entra role assignments..." -ForegroundColor Cyan
$RoleAudit = Get-MgDirectoryRole | Select DisplayName,ID,@{Name='Members';Expression={$members=Get-MgDirectoryRoleMember -DirectoryRoleId $_.Id;If ([bool]$members) {$members | Select -ExpandProperty Id} Else {$null}}}
If ([bool]$RoleAudit) {
Write-Host "Complete." -ForegroundColor Green
}
If ([bool]$EntraPimGroups) {
# Get PIM groups and the users assigned to them
Write-Host "`nRetrieving Entra PIM group assignments..." -ForegroundColor Cyan
$PIMAudit = $EntraPimGroups | ForEach-Object {Get-MgGroup -GroupId $_ |Select DisplayName,ID,@{Name='Members';Expression={$members=Get-MgGroupMember -GroupId $_.Id;If ([bool]$members) {$members | Select -ExpandProperty Id} Else {$null}}}}
If ([bool]$PIMAudit) {
Write-Host "Complete." -ForegroundColor Green
}
}
Write-Host "`nRetrieving Entra ID user details..." -ForegroundColor Cyan
# Define user properties to load
$EntraLoadProperties = 'DisplayName','UserPrincipalName','Mail','JobTitle','Department','CreatedDateTime','LastPasswordChangeDateTime','SignInActivity','AccountEnabled','AssignedLicenses','OnPremisesDistinguishedName'
# Get all member users and filter for enabled users only
$EnabledMembers = Get-MgUser -Filter "UserType eq 'Member'" -All -Property $EntraLoadProperties | Where-Object -Property AccountEnabled -eq $true
$EnabledMembers = $EnabledMembers | ForEach-Object {
$SignIn = $_.SignInActivity.LastSignInDateTime;
# Fallback to last non-interactive sign-in if no standard sign-in date is available
If ([bool]$SignIn){
$SignIn = LocalTime($SignIn)
} Else {
$SignIn = LocalTime($_.SignInActivity.LastNonInteractiveSignInDateTime)
}
[PSCustomObject]@{
UserPrincipalName = $_.UserPrincipalName
Name = $_.DisplayName
Mail = $_.Mail
# Assume shared mailbox if email attribute is set but no license
SharedMailbox = [bool]$_.Mail -and !([bool]$_.AssignedLicenses)
IdentityType = 'Cloud'
Description = $_.JobTitle
Department = $_.Department
Created = LocalTime($_.CreatedDateTime)
'Password Last Set' = LocalTime($_.LastPasswordChangeDateTime)
LastSignIn = $SignIn
'AD Admin Groups' = ''
'Entra Roles' = (($RoleAudit + $PIMAudit | Where-Object -Property Members -Contains $_.Id).DisplayName) -join ', '
OnPremisesDistinguishedName = $_.OnPremisesDistinguishedName
}
}
If ([bool]$EnabledMembers) {
Write-Host "Complete." -ForegroundColor Green
}
# Init counts
$FederatedCount = $CloudCount = 0
# Match Entra ID users with on-prem AD users
$EnabledMembers | ForEach-Object {
$Linked = $false
# If user is synced from on-prem AD
If ([bool]$_.OnPremisesDistinguishedName) {
$UPN = $_.UserPrincipalName
If ([bool]$AllUsers[$UPN]) {
# Add any Entra roles for this user
$AllUsers[$UPN].'Entra Roles' = $_.'Entra Roles'
$AllUsers[$UPN].SharedMailbox = $_.SharedMailbox
$AllUsers[$UPN].IdentityType = 'Federated'
$FederatedCount += 1
# Update last sign in if Entra has a later date
If ($AllUsers[$UPN].LastSignIn -lt $_.LastSignIn) {
$AllUsers[$UPN].LastSignIn = $_.LastSignIn
}
$Linked = $true
} Else {
# Entra user was originally synced from AD but on-prem AD account not found
$_.IdentityType = 'Orphaned'
$CloudCount += 1
}
} Else {
#On-prem only user
$OnPremCount -= 1
}
If (!([bool]$Linked)) {
#Entra only user, add to hash table as-is
$AllUsers.Add($_.UserPrincipalName, $_)
$CloudCount += 1
}
}
$PrivilegedUsers = ($AllUsers.Values | Where-Object {($_.'Entra Roles' -ne '') -or ($_.'AD Admin Groups' -ne '')})
Write-Host "`nTOTAL USERS: $($AllUsers.Count)`nOn-prem users: $($ADUsers.Count - $FederatedCount)`nFederated users: $FederatedCount`nCloud users: $CloudCount`nPrivileged users: $($PrivilegedUsers.Count)`n"
$AuditOutput = ($AllUsers.Values | Sort-Object -Property UserPrincipalName)
# Display/export output
$AuditOutput | Out-GridView
Write-Host 'See grid export for details.'
If ([bool]$OutputPath) {
# Optionally export output to file
Write-Host "`nExport details to CSV? " -ForegroundColor Cyan -NoNewline
If ((Read-Host '[y/N]').ToUpper() -eq 'Y') {
Write-Host 'Exporting to ' -NoNewline
Write-Host $OutputPath -ForegroundColor Green
$AuditOutput | Export-Csv -NoTypeInformation -Path ($OutputPath) -Encoding UTF8
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment