Powershell finds Enterprise Applications / App Registrations added by a specific user in an Entra ID tenant

<#
.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
1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading...