<#
.SYNOPSIS
Finds Enterprise Applications / App Registrations added by a specific user in an Entra ID tenant.
.DESCRIPTION
Connects to Microsoft Graph and reports on apps associated with a given user in two ways:
1. Entra ID Directory Audit Logs - "Add application" / "Add service principal" events
initiated by the user. This is the authoritative "who added it" record, but Entra
only retains these logs for a limited window (commonly ~30 days) unless exported
to a Log Analytics workspace / Sentinel.
2. Current Owners - cross-checks App Registrations and Enterprise Apps (Service
Principals) where the user is listed as an owner, since the creator is often
(but not always) added as an owner automatically. This works regardless of log
retention but is not a definitive "added by" record.
.PARAMETER UserPrincipalName
UPN or email of the user to search for, e.g. [email protected]
.PARAMETER TenantId
Optional tenant ID or verified domain (e.g. alliedhealth.onmicrosoft.com) to target a
specific tenant at sign-in time, useful if you have access to multiple tenants.
.PARAMETER DaysBack
How many days of audit log history to search. Defaults to 30 (Entra's typical retention
for directory audit logs without a Log Analytics export).
.EXAMPLE
.\Find-AppsAddedByUser.ps1 -UserPrincipalName [email protected] -TenantId alliedhealth.onmicrosoft.com
.NOTES
Requires the Microsoft.Graph PowerShell SDK module:
Install-Module Microsoft.Graph -Scope CurrentUser
Requires delegated permissions (consented at sign-in): AuditLog.Read.All, Application.Read.All, User.Read.All, Directory.Read.All
You must sign in with an account that has at least Global Reader / Application Administrator rights.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$UserPrincipalName,
[Parameter(Mandatory = $false)]
[string]$TenantId,
[Parameter(Mandatory = $false)]
[int]$DaysBack = 30
)
$requiredScopes = @('AuditLog.Read.All', 'Application.Read.All', 'User.Read.All', 'Directory.Read.All')
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) {
Write-Host "Microsoft.Graph module not found. Installing for current user..." -ForegroundColor Yellow
Install-Module Microsoft.Graph -Scope CurrentUser -Force -AllowClobber
}
$connectParams = @{ Scopes = $requiredScopes }
if ($TenantId) { $connectParams['TenantId'] = $TenantId }
Write-Host "Signing in to Microsoft Graph..." -ForegroundColor Cyan
Connect-MgGraph @connectParams | Out-Null
$context = Get-MgContext
Write-Host "Connected to tenant: $($context.TenantId)" -ForegroundColor Green
# Resolve the target user so we can match on both objectId and UPN in audit logs
$targetUser = Get-MgUser -Filter "userPrincipalName eq '$UserPrincipalName'" -ErrorAction SilentlyContinue
if (-not $targetUser) {
$targetUser = Get-MgUser -Filter "mail eq '$UserPrincipalName'" -ErrorAction SilentlyContinue
}
if (-not $targetUser) {
Write-Warning "Could not resolve user '$UserPrincipalName' via Get-MgUser. Falling back to display-name text match in audit logs only."
}
else {
Write-Host "Resolved user: $($targetUser.DisplayName) ($($targetUser.Id))" -ForegroundColor Green
}
# ---------------------------------------------------------------------------
# 1. Directory audit log search - "Add application" / "Add service principal"
# ---------------------------------------------------------------------------
Write-Host "`n=== Searching audit logs for the last $DaysBack day(s) ===" -ForegroundColor Cyan
$startDate = (Get-Date).AddDays(-$DaysBack).ToString('yyyy-MM-ddTHH:mm:ssZ')
$activities = @('Add application', 'Add service principal', 'Add application permissions to service principal')
$auditFilter = "activityDateTime ge $startDate"
$auditResults = Get-MgAuditLogDirectoryAudit -Filter $auditFilter -All -ErrorAction SilentlyContinue
$matchedAudits = @()
if ($auditResults) {
$matchedAudits = $auditResults | Where-Object {
$activities -contains $_.ActivityDisplayName -and (
($targetUser -and $_.InitiatedBy.User.Id -eq $targetUser.Id) -or
($_.InitiatedBy.User.UserPrincipalName -eq $UserPrincipalName) -or
($_.InitiatedBy.User.DisplayName -match [regex]::Escape($UserPrincipalName))
)
}
}
if ($matchedAudits.Count -eq 0) {
Write-Host "No matching 'Add application' / 'Add service principal' audit events found in the last $DaysBack days." -ForegroundColor Yellow
Write-Host "(Entra audit log retention is short by default - widen -DaysBack won't help beyond the tenant's actual retention window.)" -ForegroundColor Yellow
}
else {
$auditReport = $matchedAudits | ForEach-Object {
[PSCustomObject]@{
DateTime = $_.ActivityDateTime
Activity = $_.ActivityDisplayName
TargetName = ($_.TargetResources | Select-Object -First 1).DisplayName
TargetType = ($_.TargetResources | Select-Object -First 1).Type
TargetAppId = ($_.TargetResources | Select-Object -First 1).Id
InitiatedBy = $_.InitiatedBy.User.UserPrincipalName
}
}
$auditReport | Sort-Object DateTime -Descending | Format-Table -AutoSize
}
# ---------------------------------------------------------------------------
# 2. Cross-check: current owners of App Registrations and Enterprise Apps
# ---------------------------------------------------------------------------
Write-Host "`n=== Cross-checking current App Registration / Enterprise App owners ===" -ForegroundColor Cyan
$ownedApps = @()
$ownedSPs = @()
if ($targetUser) {
$ownedObjects = Get-MgUserOwnedObject -UserId $targetUser.Id -All -ErrorAction SilentlyContinue
$ownedApps = $ownedObjects | Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.application' }
$ownedSPs = $ownedObjects | Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.servicePrincipal' }
}
if ($ownedApps.Count -eq 0 -and $ownedSPs.Count -eq 0) {
Write-Host "No App Registrations or Enterprise Apps found where this user is a current owner." -ForegroundColor Yellow
}
else {
if ($ownedApps.Count -gt 0) {
Write-Host "`nApp Registrations owned by $($targetUser.DisplayName):" -ForegroundColor Green
$ownedApps | ForEach-Object {
[PSCustomObject]@{
DisplayName = $_.AdditionalProperties['displayName']
AppId = $_.AdditionalProperties['appId']
Id = $_.Id
}
} | Format-Table -AutoSize
}
if ($ownedSPs.Count -gt 0) {
Write-Host "`nEnterprise Apps (Service Principals) owned by $($targetUser.DisplayName):" -ForegroundColor Green
$ownedSPs | ForEach-Object {
[PSCustomObject]@{
DisplayName = $_.AdditionalProperties['displayName']
AppId = $_.AdditionalProperties['appId']
Id = $_.Id
}
} | Format-Table -AutoSize
}
}
# ---------------------------------------------------------------------------
# 3. Apps where the user is an assigned user (Enterprise Apps > Users and groups)
# ---------------------------------------------------------------------------
Write-Host "`n=== Apps where $UserPrincipalName is an assigned user ===" -ForegroundColor Cyan
if ($targetUser) {
$assignments = Get-MgUserAppRoleAssignment -UserId $targetUser.Id -All -ErrorAction SilentlyContinue
if (-not $assignments -or $assignments.Count -eq 0) {
Write-Host "No direct app role assignments found for this user." -ForegroundColor Yellow
}
else {
$assignments | ForEach-Object {
[PSCustomObject]@{
AppName = $_.ResourceDisplayName
ResourceId = $_.ResourceId
AppRoleId = $_.AppRoleId
AssignedOn = $_.CreatedDateTime
PrincipalType = $_.PrincipalType
}
} | Sort-Object AppName | Format-Table -AutoSize
}
}
else {
Write-Warning "Skipping assigned-apps check - user could not be resolved."
}
Write-Host "`nDone. Disconnecting from Microsoft Graph." -ForegroundColor Cyan
Disconnect-MgGraph | Out-Null