Collectors/Applications.ps1

@{
    Name           = 'Applications'
    FileName       = 'applications'
    ApiVersion     = 'v1.0'
    RequiredScopes = @('Application.Read.All')
    Description    = 'App registrations (credential expiry, requested high-privilege Graph application permissions, owners) and service principals.'
    Collect        = {
        $applications = @(Invoke-TLGraphRequest -Uri '/v1.0/applications?$expand=owners($select=id,displayName,userPrincipalName)' -All)
        $servicePrincipals = @(Invoke-TLGraphRequest -Uri '/v1.0/servicePrincipals?$select=id,appId,displayName,accountEnabled,servicePrincipalType&$top=999' -All)

        # Well-known high-privilege Microsoft Graph application role ids
        $graphResourceAppId = '00000003-0000-0000-c000-000000000000'
        $riskyGraphAppRoles = @{
            '19dbc75e-c2e2-444c-a770-ec69d8559fc7' = 'Directory.ReadWrite.All'
            '1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9' = 'Application.ReadWrite.All'
            '9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8' = 'RoleManagement.ReadWrite.Directory'
            '06b708a9-e830-4db3-a914-8e69da51d44f' = 'AppRoleAssignment.ReadWrite.All'
            '741f803b-c850-494e-b5df-cde7c675a1ca' = 'User.ReadWrite.All'
            'e2a3a72e-5f79-4c64-b1b1-878b674786c9' = 'Mail.ReadWrite'
            'b633e1c5-b582-4048-a93e-9f11b44c7e96' = 'Mail.Send'
            '75359482-378d-4052-8f01-80520e7db3cd' = 'Files.ReadWrite.All'
            '62a82d76-70ea-41e2-9197-370581804d09' = 'Group.ReadWrite.All'
        }
        $utcNow = [DateTime]::UtcNow

        foreach ($application in $applications) {
            $total = 0; $expired = 0; $expiringSoon = 0; $passwordCount = 0
            $soonest = $null
            foreach ($credential in (@($application.passwordCredentials) + @($application.keyCredentials))) {
                if ($null -eq $credential) { continue }
                $total++
                $endsAt = $null
                if ($credential.PSObject.Properties['endDateTime'] -and $credential.endDateTime) {
                    $endsAt = ([DateTime]$credential.endDateTime).ToUniversalTime()
                }
                if ($null -eq $endsAt) { continue }
                if ($null -eq $soonest -or $endsAt -lt $soonest) { $soonest = $endsAt }
                if ($endsAt -lt $utcNow) { $expired++ }
                elseif ($endsAt -lt $utcNow.AddDays(30)) { $expiringSoon++ }
            }
            $passwordCount = @($application.passwordCredentials).Count

            Add-Member -InputObject $application -NotePropertyName '_tlCredentialStatus' -NotePropertyValue ([pscustomobject]@{
                    total                = $total
                    passwordCredentials  = $passwordCount
                    expired              = $expired
                    expiringWithin30Days = $expiringSoon
                    soonestEndUtc        = $(if ($soonest) { $soonest.ToString('o') } else { $null })
                }) -Force

            $risky = @()
            foreach ($resource in @($application.requiredResourceAccess)) {
                if ($null -eq $resource -or [string]$resource.resourceAppId -ne $graphResourceAppId) { continue }
                foreach ($access in @($resource.resourceAccess)) {
                    if ([string]$access.type -eq 'Role' -and $riskyGraphAppRoles.ContainsKey([string]$access.id)) {
                        $risky += $riskyGraphAppRoles[[string]$access.id]
                    }
                }
            }
            Add-Member -InputObject $application -NotePropertyName '_tlRiskyPermissions' -NotePropertyValue @($risky | Sort-Object -Unique) -Force
            Add-Member -InputObject $application -NotePropertyName '_tlOwnerCount' -NotePropertyValue (@($application.owners).Count) -Force
        }

        @{
            applications      = $applications
            servicePrincipals = $servicePrincipals
        }
    }
}