Invoke-EntraAppReport.ps1

<#PSScriptInfo
 
.VERSION 0.1.0
 
.GUID 175aa966-47dc-4a76-bbd1-b7ab11cd3079
 
.AUTHOR Daniel Bradley
 
.COMPANYNAME ourcloudnetwork.co.uk
 
.COPYRIGHT
 
.TAGS
    ourcloudnetwork
    Microsoft Entra
    Microsoft Graph
 
.LICENSEURI
 
.PROJECTURI
    https://ourcloudnetwork.com/create-a-free-enterprise-app-permissions-report-in-microsoft-entra/
 
.EXTERNALMODULEDEPENDENCIES
    Microsoft.Graph.Authentication
 
.RELEASENOTES
    v0.1 - Initial release
#>


<#
.DESCRIPTION
 This script, created by Daniel Bradley at ourcloudnetwork.co.uk, generates a report of all applications in your Microsoft Entra tenant, including their permissions, credentials, and sign-in activity. The report includes a summary of the total number of applications, applications with delegated permissions, applications with application permissions, third-party applications, and inactive applications. The report also includes a list of applications with insecure sign-in methods, applications with application permissions, and applications with no sign-in activity. The report is generated in HTML format and can be saved to a file for further analysis.
 
.PARAMETER outpath
 Specified the output path of the report file.
 
.EXAMPLE
PS> Invoke-EntraAuthReport -outpath "C:\Reports\EntraAuthReport.html"
#>


[CmdletBinding()]
param(
     [Parameter()]
     [ValidateNotNullOrEmpty()]
     [string]$outpath = "C:\temp"
)

# Display script start message
Write-Host "Starting Entra Application Permissions Report..." -ForegroundColor Cyan

# Check Microsoft Graph connection
Write-Host "Checking Microsoft Graph connection status..." -ForegroundColor Yellow
$state = Get-MgContext

# Define required permissions properly as an array of strings
$requiredPerms = @("AuditLog.Read.All","Organization.Read.All","Application.Read.All","Directory.Read.All")

# Check if we're connected and have all required permissions
$hasAllPerms = $false
if ($state) {
    $missingPerms = @()
    foreach ($perm in $requiredPerms) {
        if ($state.Scopes -notcontains $perm) {
            $missingPerms += $perm
        }
    }
    if ($missingPerms.Count -eq 0) {
        $hasAllPerms = $true
        Write-output "Connected to Microsoft Graph with all required permissions"
    } else {
        Write-output "Missing required permissions: $($missingPerms -join ', ')"
        Write-output "Reconnecting with all required permissions..."
    }
} else {
    Write-output "Not connected to Microsoft Graph. Connecting now..."
}

# Connect if we need to
if (-not $hasAllPerms) {
    try {
        Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow
        Connect-MgGraph -Scopes $requiredPerms -ErrorAction Stop -NoWelcome
        Write-output "Successfully connected to Microsoft Graph"
    } catch {
        Write-Error "Failed to connect to Microsoft Graph: $_"
        exit
    }
}

# Initialize progress counter
$progressSteps = 8
$currentStep = 0

#Get organisation information
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Getting organization information" -PercentComplete (($currentStep / $progressSteps) * 100)
$organisationInfo = (Invoke-MgGraphRequest -Uri "v1.0/organization" -OutputType PSObject | Select -Expand value)

#Get the Graph Service Principal ID
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Getting Graph service principal" -PercentComplete (($currentStep / $progressSteps) * 100)
$graphSp = Invoke-MgGraphRequest -Uri "v1.0/servicePrincipals(appId='{00000003-0000-0000-c000-000000000000}')?`$select=id,appRoles" -OutputType PSObject
 
#Get all OAuth2 delegated Graph permissions for all apps
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Getting OAuth2 delegated permissions" -PercentComplete (($currentStep / $progressSteps) * 100)
Write-Host "Retrieving OAuth2 delegated permissions..." -ForegroundColor Yellow
$uri = "v1.0/oauth2PermissionGrants?`$filter=ConsentType eq 'AllPrincipals' and resourceId eq '$($graphSp.Id)'&`$top=999"
$Result = Invoke-MgGraphRequest -Uri $Uri -OutputType PSObject
$coreDelegatedPermissions = $Result.value
$NextLink = $Result."@odata.nextLink"
while ($NextLink -ne $null) {
    $Result = Invoke-MgGraphRequest -Method GET -Uri $NextLink -OutputType PSObject
    $coredelegatedPermissions += $Result.value
    $NextLink = $Result."@odata.nextLink"
}
$delegatedPermissions = $coreDelegatedPermissions | Select-Object @{Name='ServicePrincipalId';Expression={$_.ClientId}}, @{Name='ScopeType';Expression={"Delegated"}}, @{Name='Scope'; Expression={$_.Scope -split ' ' -ne '' }}

#Get all possible available app roles
$graphAppRoles = $graphSp | Select-Object -ExpandProperty AppRoles | Select-Object Id, Value | Group-Object -Property Id -AsHashTable

#Get all app role assignments for all apps
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Getting application role assignments" -PercentComplete (($currentStep / $progressSteps) * 100)
Write-Host "Retrieving application role assignments..." -ForegroundColor Yellow
$uri = "/v1.0/servicePrincipals?`$expand=appRoleAssignments&`$select=id,appId,displayName,appOwnerOrganizationId,appId&`$top=999"
$Result = Invoke-MgGraphRequest -Uri $Uri -OutputType PSObject
$coreRoleAssignments = $Result.value
$NextLink = $Result."@odata.nextLink"
while ($NextLink -ne $null) {
    $Result = Invoke-MgGraphRequest -Method GET -Uri $NextLink -OutputType PSObject
    $coreRoleAssignments += $Result.value
    $NextLink = $Result."@odata.nextLink"
}
$spRoleAssignments = $coreRoleAssignments | Where-Object { ($_.AppRoleAssignments | Where-Object { $_.ResourceId -eq $graphSp.Id }).Count -gt 0 } | Select-Object Id, appOwnerOrganizationId, AppId, DisplayName, @{Name="AppRoleId"; Expression={ ($_.AppRoleAssignments).AppRoleId }}

#Expand permissions for all app role assignments
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Processing application permissions" -PercentComplete (($currentStep / $progressSteps) * 100)
Write-Host "Processing application permissions..." -ForegroundColor Yellow
$appPermissions = $spRoleAssignments | ForEach-Object { [PSCustomObject]@{DisplayName = $_.DisplayName; ServicePrincipalId = $_.Id; AppId = $_.appId; appOwnerOrganizationId = $_.appOwnerOrganizationId; ScopeType="Application"; Scope = @($graphAppRoles[$_.AppRoleId].Value)}}

#Update delegated permissions with appOwnerOrganizationId and DisplayName
Write-Host "Updating delegated permission details..." -ForegroundColor Yellow
$delegatedPermissions | ForEach-Object{
    $item = $_
    $_ | Add-Member -MemberType NoteProperty -Name "appOwnerOrganizationId" -Value ($coreRoleAssignments | Where-Object {$_.Id -eq $item.ServicePrincipalId}).appOwnerOrganizationId
    $_ | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value ($coreRoleAssignments | Where-Object {$_.Id -eq $item.ServicePrincipalId}).DisplayName
    $_ | Add-Member -MemberType NoteProperty -Name "appId" -Value ($coreRoleAssignments | Where-Object {$_.Id -eq $item.ServicePrincipalId}).appId
}

#Combine all permissions
$AllfilteredPermissions = ($delegatedPermissions + $appPermissions) | Select-Object DisplayName, ServicePrincipalId, appId, appOwnerOrganizationId, ScopeType, Scope | Sort-Object DisplayName

#Get ServicePrincipal Sign-in activity report
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Getting service principal sign-in activity" -PercentComplete (($currentStep / $progressSteps) * 100)
Write-Host "Retrieving service principal sign-in activity..." -ForegroundColor Yellow
$SignInActivityReport = Invoke-MgGraphRequest -Uri "/beta/reports/servicePrincipalSignInActivities" -OutputType PSObject | Select -Expand Value

#Function to get last sign-in activity
function GetLastActivityDate {
    param (
        [string]$appId
    )
    $lastsignin = $SignInActivityReport | Where-Object { $_.appId -eq $appId}
    $obj = [PSCustomObject]@{DelegatedLastSignIn =  $lastsignin.delegatedClientSignInActivity.lastSignInDateTime; ApplicationLastSignIn = $lastsignin.applicationAuthenticationClientSignInActivity.lastSignInDateTime}
    return $obj
}

# Get Service Principal Audit Logs
Function Get-MgSpSignIns {
    param(
        $filter
    )

    process {
        $response = Invoke-MgGraphRequest -uri "https://graph.microsoft.com/beta/auditLogs/signIns?&source=sp&`$filter=$filter" -OutputType PSObject | Select -Expand Value
        return $response
    }
}
$ServicePrincipalSignIns = Get-MgSpSignIns -filter "appOwnerTenantId ne '$($organisationInfo.id)'" | Select createdDateTime, appDisplayName, appId, clientCredentialType, appOwnerTenantId

#Add last sign-in activity to the permissions report
Write-Host "Adding sign-in activity data to applications..." -ForegroundColor Yellow
$AllfilteredPermissions | ForEach-Object {
    $app = $_
    $SignInInfo = GetLastActivityDate -appId $_.appId
    $LastSigninType = ($ServicePrincipalSignIns | Where-Object { $_.appId -eq $app.appId } | Sort-Object createdDateTime -Descending | Select-Object -First 1).clientCredentialType
    $_ | Add-Member -MemberType NoteProperty -Name "DelegatedLastSignIn" -Value $SignInInfo.DelegatedLastSignIn
    $_ | Add-Member -MemberType NoteProperty -Name "ApplicationLastSignIn" -Value $SignInInfo.ApplicationLastSignIn
    $_ | Add-Member -MemberType NoteProperty -Name "LastSignInType" -Value $LastSigninType
}

#Get password and key credentials for first-party applications
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Getting application credentials" -PercentComplete (($currentStep / $progressSteps) * 100)
Write-Host "Retrieving application credentials..." -ForegroundColor Yellow
$credentials = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/applications/?`$select=displayName,id,appId,info,createdDateTime,keyCredentials,passwordCredentials,deletedDateTime&`$count=true" -OutputType PSObject | Select -Expand Value

$AllfilteredPermissions | ForEach-Object {
    $app = $_
    $appcredentials = $credentials | Where-Object { $_.appId -eq $app.appId }
    $credentialsToAdd = @()
    $KeyCert = $($appcredentials.keyCredentials).count
    $Password = $($appcredentials.passwordCredentials).count
    if ($($appcredentials.keyCredentials).count -gt 0) {
        $credentialsToAdd += "Client Certificate"
    }
    if ($($appcredentials.passwordCredentials).count -gt 0) {
        $credentialsToAdd += "Client Secret"
    }
    $_ | Add-Member -MemberType NoteProperty -Name "App Credentials" -Value $credentialsToAdd
}

# Create additional stats
$currentStep++
Write-Progress -Activity "Gathering Entra Application Data" -Status "Creating security indicators" -PercentComplete (($currentStep / $progressSteps) * 100)
Write-Host "Generating security risk indicators..." -ForegroundColor Yellow

# Third-party apps with insecure sign-in methods
$WeakSignInMethods = @("clientSecret","clientAssertion","certificate")
$WeakAppCredentials = @("Client Secret","Client Certificate")
$InsecureThirdPartyApps = $AllfilteredPermissions | Where { ($_.appOwnerOrganizationId -ne $organisationInfo.id) -and ($_.LastSignInType -in $WeakSignInMethods) } | Select DisplayName -Unique
# first-party apps with insecure sign-in methods
$InsecureFirstPartyApps = $AllfilteredPermissions | Where { ($_.appOwnerOrganizationId -eq $organisationInfo.id) -and ($_.'App Credentials' -in $WeakAppCredentials) } | Select DisplayName -Unique
# Third-party apps with application permissions
$ThirdPartyAppsWithAppPermissions = $AllfilteredPermissions | Where { ($_.appOwnerOrganizationId -ne $organisationInfo.id) -and ($_.ScopeType -eq "Application") } | Select DisplayName -Unique
# Applications with no sign-in activity
$NonActiveApps = $AllfilteredPermissions | Where { ($null -eq $_.DelegatedLastSignIn) -and ($null -eq $_.ApplicationLastSignIn) } | Select DisplayName -Unique

# Generate HTML Report for Entra Applications
function Export-EntraAppHTMLReport {
    param (
        [Parameter(Mandatory = $true)]
        [Array]$ReportData,
        
        [Parameter(Mandatory = $false)]
        [string]$ReportTitle = "Entra Application Permissions Report",
        
        [Parameter(Mandatory = $false)]
        [string]$OutputPath
    )
    
    # Define CSS for the report
    $css = @"
    <style>
        :root {
            --primary-color: #2563EB;
            --secondary-color: #1E40AF;
            --accent-color: #3B82F6;
            --background-color: #f8f9fa;
            --table-header-bg: #f2f2f2;
            --table-border: #ddd;
            --table-hover: #f1f1f1;
            --warning-color: #e74c3c;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            color: #333;
            margin: 0;
            padding: 0;
            background-color: var(--background-color);
        }
        .container {
            width: 95%;
            margin: 20px auto;
            background-color: white;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            border-radius: 8px;
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
            position: relative;
            padding: 25px 20px;
            color: white;
            overflow: hidden;
        }
        .header::before {
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: radial-gradient(circle at 15% 50%, rgba(59, 130, 246, 0.3) 0%, transparent 50%),
                        radial-gradient(circle at 85% 30%, rgba(37, 99, 235, 0.2) 0%, transparent 50%);
        }
        .header h1 {
            margin: 0;
            font-size: 28px;
            font-weight: 600;
            position: relative;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }
        .header-subtitle {
            font-size: 14px;
            font-weight: 400;
            margin-top: 0px;
            margin-bottom: 10px;
            opacity: 0.9;
        }
        .header-content {
            display: flex;
            justify-content: space-between;
            position: relative;
        }
        .header-left-content {
            flex: 1;
        }
        .header-details {
            display: flex;
            justify-content: space-between;
            margin-top: 15px;
            font-size: 14px;
            position: relative;
            opacity: 0.9;
        }
        .author-info {
            margin-top: 12px;
            border-top: 1px solid rgba(255, 255, 255, 0.3);
            padding-top: 10px;
            display: flex;
            align-items: center;
            font-size: 13px;
        }
        .author-label {
            opacity: 0.8;
            margin-right: 6px;
        }
        .author-links {
            display: flex;
            align-items: center;
        }
        .author-link {
            color: white !important; /* Ensure text color is always white */
            text-decoration: none !important; /* Remove underline */
            display: inline-flex;
            align-items: center;
            border: 1px solid rgba(255, 255, 255, 0.5);
            padding: 4px 10px;
            border-radius: 4px;
            margin-right: 10px;
            transition: all 0.3s ease;
            background-color: rgba(255, 255, 255, 0.1);
            cursor: pointer;
            z-index: 10; /* Ensure links are above other elements */
            position: relative; /* Establish stacking context */
        }
        .author-link:hover {
            background-color: rgba(255, 255, 255, 0.3);
            border-color: rgba(255, 255, 255, 0.9);
            transform: translateY(-2px);
            box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
        }
        .author-link:active {
            transform: translateY(0);
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
        }
        .author-link svg {
            margin-right: 5px;
            flex-shrink: 0;
        }
        .report-info {
            text-align: right;
            font-size: 14px;
            display: flex;
            flex-direction: column;
            justify-content: flex-end;
            padding-bottom: 10px;
        }
        .report-date {
            font-weight: 500;
            margin-top: 5px;
        }
        .stats-container {
            display: flex;
            justify-content: space-between;
            margin: 20px;
            flex-wrap: wrap;
            gap: 15px;
        }
        .stat-box {
            background-color: white;
            border-radius: 10px;
            padding: 18px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
            flex: 1;
            min-width: 200px;
            transition: transform 0.2s ease, box-shadow 0.2s ease;
            border: none;
            position: relative;
            overflow: hidden;
        }
        .stat-box::after {
            content: '';
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 3px;
            background: linear-gradient(90deg,
                rgba(37, 99, 235, 0.8),
                rgba(59, 130, 246, 0.6),
                rgba(96, 165, 250, 0.4));
            border-bottom-left-radius: 10px;
            border-bottom-right-radius: 10px;
        }
        .stat-box:hover {
            transform: translateY(-3px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }
        .stat-box h3 {
            margin-top: 0;
            color: #4b5563;
            font-size: 14px;
            font-weight: 500;
            letter-spacing: 0.3px;
            margin-bottom: 15px;
        }
        .stat-box p {
            margin-bottom: 0;
            font-size: 26px;
            font-weight: 600;
            color: #1f2937;
            background: linear-gradient(90deg, #2563EB, #4f46e5);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            text-fill-color: transparent;
        }
        .controls {
            padding: 15px 20px;
            background-color: #fff;
            border-bottom: 1px solid var(--table-border);
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            align-items: center;
        }
        .search-box {
            flex-grow: 1;
            position: relative;
            min-width: 250px;
        }
        .search-box input {
            width: 100%;
            padding: 8px 12px 8px 35px;
            border: 1px solid var(--table-border);
            border-radius: 4px;
            font-size: 14px;
            box-sizing: border-box;
        }
        .search-box::before {
            content: "🔍";
            position: absolute;
            left: 10px;
            top: 50%;
            transform: translateY(-50%);
            color: #999;
        }
        select, button {
            padding: 8px 12px;
            border: 1px solid var(--table-border);
            border-radius: 4px;
            background-color: white;
            font-size: 14px;
        }
        button {
            cursor: pointer;
            background-color: var(--primary-color);
            color: white;
            border: none;
            transition: background-color 0.2s;
        }
        button:hover {
            background-color: var(--secondary-color);
        }
        .table-container {
            padding: 0 20px 20px;
            overflow-x: auto;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            font-size: 14px;
        }
        th {
            background-color: var(--table-header-bg);
            color: #333;
            font-weight: 600;
            padding: 12px 15px;
            text-align: left;
            position: sticky;
            top: 0;
            cursor: pointer;
            user-select: none;
            border-bottom: 2px solid var(--table-border);
        }
        th:hover {
            background-color: #e6e6e6;
        }
        td {
            padding: 10px 15px;
            border-bottom: 1px solid var(--table-border);
            vertical-align: top;
        }
        tr:hover {
            background-color: var(--table-hover);
        }
        .permission-cell {
            max-width: 300px;
        }
        .permission-list {
            margin: 0;
            padding-left: 20px;
            word-break: break-word;
        }
        .date-cell {
            white-space: nowrap;
        }
        .no-activity {
            color: #999;
            font-style: italic;
        }
        .third-party {
            background-color: rgba(231, 76, 60, 0.1);
        }
        .third-party td:first-child::before {
            margin-right: 5px;
        }
        .tooltip {
            position: relative;
            display: inline-block;
            cursor: help;
        }
        .tooltip .tooltiptext {
            visibility: hidden;
            width: 200px;
            background-color: #333;
            color: #fff;
            text-align: center;
            border-radius: 6px;
            padding: 5px;
            position: absolute;
            z-index: 1;
            bottom: 125%;
            left: 50%;
            transform: translateX(-50%);
            opacity: 0;
            transition: opacity 0.3s;
        }
        .tooltip:hover .tooltiptext {
            visibility: visible;
            opacity: 1;
        }
        .footer {
            margin: 20px;
            text-align: center;
            font-size: 12px;
            color: #666;
            border-top: 1px solid var(--table-border);
            padding-top: 20px;
        }
        @media print {
            body {
                background-color: white;
            }
            .container {
                width: 100%;
                box-shadow: none;
            }
            .controls, button {
                display: none;
            }
            .tooltip .tooltiptext {
                display: none;
            }
        }
        @media (max-width: 768px) {
            .stats-container {
                flex-direction: column;
            }
            .stat-box {
                margin-bottom: 10px;
            }
            .header-details {
                flex-direction: column;
            }
        }
        .sort-icon::after {
            content: "↕️";
            font-size: 12px;
            margin-left: 5px;
        }
        .sort-asc::after {
            content: "↑";
        }
        .sort-desc::after {
            content: "↓";
        }
         
        /* Add new styles for red flags section */
        .red-flag-container {
            background-color: #FEECED;
            border-left: 4px solid #CD0000;
            margin: 20px;
            padding: 15px 20px;
            border-radius: 5px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1);
        }
         
        .red-flag-title {
            color: #CD0000;
            font-weight: 600;
            margin-top: 0;
            margin-bottom: 15px;
            display: flex;
            align-items: center;
        }
         
        .red-flag-title svg {
            margin-right: 10px;
            flex-shrink: 0;
        }
         
        .red-flag-item {
            margin-bottom: 15px;
            padding-bottom: 15px;
            border-bottom: 1px solid rgba(205, 0, 0, 0.2);
        }
         
        .red-flag-item:last-child {
            margin-bottom: 0;
            padding-bottom: 0;
            border-bottom: none;
        }
         
        .red-flag-heading {
            color: #CD0000;
            font-weight: 600;
            margin-top: 0;
            margin-bottom: 5px;
            font-size: 15px;
        }
         
        .red-flag-desc {
            margin-top: 0;
            margin-bottom: 10px;
        }
         
        .red-flag-count {
            display: inline-block;
            background-color: #CD0000;
            color: white;
            font-weight: bold;
            padding: 3px 8px;
            border-radius: 12px;
            font-size: 12px;
            margin-left: 8px;
        }
         
        .red-flag-apps {
            background-color: #FFDAD9;
            padding: 10px;
            border-radius: 4px;
            max-height: 120px;
            overflow-y: auto;
            font-size: 13px;
        }
         
        .red-flag-apps ul {
            margin: 0;
            padding-left: 20px;
        }
         
        .red-flag-apps li {
            margin-bottom: 3px;
        }
         
        .red-flag-link {
            color: #CD0000;
            cursor: pointer;
            text-decoration: underline;
            font-size: 13px;
        }
    </style>
"@


    # Format dates for display
    function Format-DateForDisplay {
        param($date)
        if ($null -eq $date -or $date -eq "") {
            return "<span class='no-activity'>No activity</span>"
        }
        else {
            try {
                $dt = [datetime]$date
                return $dt.ToString("yyyy-MM-dd HH:mm")
            }
            catch {
                return "<span class='no-activity'>Invalid date</span>"
            }
        }
    }

    # Calculate statistics for the report
    $totalApps = ($ReportData | Select-Object -Unique AppId).Count
    $delegatedApps = ($ReportData | Where-Object {$_.ScopeType -eq "Delegated"} | Select-Object -Unique AppId).Count
    $applicationApps = ($ReportData | Where-Object {$_.ScopeType -eq "Application"} | Select-Object -Unique AppId).Count
    $thirdPartyApps = ($ReportData | Where-Object {$_.appOwnerOrganizationId -ne $organisationInfo.id -and $null -ne $_.appOwnerOrganizationId} | Select-Object -Unique AppId).Count
    $inactiveApps = ($ReportData | Where-Object {$null -eq $_.DelegatedLastSignIn -and $null -eq $_.ApplicationLastSignIn} | Select-Object -Unique AppId).Count

    # Create rows for the report
    $tableRows = ""
    foreach ($item in $ReportData) {
        $scopeList = ""
        if ($null -ne $item.Scope -and $item.Scope.Count -gt 0) {
            $scopeList = "<ul class='permission-list'>"
            foreach ($scope in $item.Scope) {
                $scopeList += "<li>$scope</li>"
            }
            $scopeList += "</ul>"
        }
        else {
            $scopeList = "<span class='no-activity'>None</span>"
        }
        
        # Format App Credentials as bullet points similar to permissions
        $credentialsList = ""
        if ($null -ne $item.'App Credentials' -and $item.'App Credentials'.Count -gt 0) {
            $credentialsList = "<ul class='permission-list'>"
            foreach ($credential in $item.'App Credentials') {
                $credentialsList += "<li>$credential</li>"
            }
            $credentialsList += "</ul>"
        }
        else {
            $credentialsList = "<span class='no-activity'>None</span>"
        }
        
        $delegatedLastSignIn = Format-DateForDisplay $item.DelegatedLastSignIn
        $applicationLastSignIn = Format-DateForDisplay $item.ApplicationLastSignIn
        $lastSignInType = if ($item.LastSignInType) { $item.LastSignInType } else { "<span class='no-activity'>Unknown</span>" }
        
        # Determine app source and apply styling based on criteria
        $appSource = "First Party"
        $rowClass = ""
        $warningIcon = ""
        
        # Check if it's a third-party application
        $isThirdParty = (($null -eq $item.appOwnerOrganizationId) -or ($item.appOwnerOrganizationId -ne $organisationInfo.id))
        
        if ($isThirdParty) {
            $appSource = "Third Party"
            $rowClass = "class='third-party'"
            
            # Only add warning icon for third-party apps with application permissions
            if ($item.ScopeType -eq "Application") {
                $warningIcon = "⚠️ "
            }
        }
        
        $tableRows += @"
        <tr $rowClass>
            <td>$warningIcon$($item.DisplayName)</td>
            <td>$($item.appId)</td>
            <td>$appSource</td>
            <td>$($item.ScopeType)</td>
            <td class="permission-cell">$scopeList</td>
            <td class="date-cell">$delegatedLastSignIn</td>
            <td class="date-cell">$applicationLastSignIn</td>
            <td>$lastSignInType</td>
            <td class="credential-cell">$credentialsList</td>
        </tr>
"@

    }

    # Create the HTML
    $html = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>$ReportTitle</title>
    $css
</head>
<body>
    <div class="container">
        <div class="header">
            <div class="header-content">
                <div class="header-left-content">
                    <h1>$ReportTitle</h1>
                    <div class="header-subtitle">Overview of application permissions in your tenant</div>
                    <div class="author-info">
                        <span class="author-label">Created by:</span>
                        <div class="author-links">
                            <a href="https://www.linkedin.com/in/danielbradley2/" class="author-link" onclick="window.open('https://www.linkedin.com/in/danielbradley2/', '_blank'); return false;">
                                <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="white">
                                    <path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
                                </svg>
                                Daniel Bradley
                            </a>
                            <a href="https://ourcloudnetwork.com" class="author-link" onclick="window.open('https://ourcloudnetwork.com', '_blank'); return false;">
                                <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="white">
                                    <path d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035l4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"/>
                                </svg>
                                ourcloudnetwork.com
                            </a>
                        </div>
                    </div>
                </div>
                <div class="report-info">
                    <div class="report-date">Generated: $(Get-Date -Format "MMMM d, yyyy")</div>
                    <div class="tenant">Tenant ID: $($organisationInfo.id)</div>
                </div>
            </div>
        </div>
 
        <div class="stats-container">
            <div class="stat-box">
                <h3>Total Applications</h3>
                <p>$totalApps</p>
            </div>
            <div class="stat-box">
                <h3>Apps with Delegated Permissions</h3>
                <p>$delegatedApps</p>
            </div>
            <div class="stat-box">
                <h3>Apps with Application Permissions</h3>
                <p>$applicationApps</p>
            </div>
            <div class="stat-box">
                <h3>Third-Party Apps</h3>
                <p>$thirdPartyApps</p>
            </div>
            <div class="stat-box">
                <h3>Apps with No Sign-In Activity</h3>
                <p>$inactiveApps</p>
            </div>
        </div>
         
"@


    # Add Red Flag section if needed
    if ($InsecureThirdPartyApps.Count -gt 0 -or $InsecureFirstPartyApps.Count -gt 0 -or $ThirdPartyAppsWithAppPermissions.Count -gt 0 -or $NonActiveApps.Count -gt 0) {
        $html += @"
        <div class="red-flag-container">
            <h3 class="red-flag-title">
                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#CD0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
                    <line x1="12" y1="9" x2="12" y2="13"></line>
                    <line x1="12" y1="17" x2="12.01" y2="17"></line>
                </svg>
                Security Risk Indicators
            </h3>
"@

        
        if ($InsecureThirdPartyApps.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">Third-Party Applications Using Insecure Authentication <span class="red-flag-count">$($InsecureThirdPartyApps.Count)</span></h4>
                <p class="red-flag-desc">These third-party applications are using insecure authentication methods (client secrets, certificates or client assertion) which may pose security risks. Work with these vendors to assess the risk.</p>
                <div style="margin-top: 8px;">
                    <span class="red-flag-link" onclick="filterByInsecureThirdPartyApps()">Filter to show these apps</span>
                </div>
            </div>
"@

        }
        
        if ($InsecureFirstPartyApps.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">First-Party Applications Using Insecure Authentication <span class="red-flag-count">$($InsecureFirstPartyApps.Count)</span></h4>
                <p class="red-flag-desc">These first-party applications are using insecure authentication methods (client secrets or certificates) which may pose security risks. Consider using a Managed Identity.</p>
                <div style="margin-top: 8px;">
                    <span class="red-flag-link" onclick="filterByInsecureFirstPartyApps()">Filter to show these apps</span>
                </div>
            </div>
"@

        }
        
        if ($ThirdPartyAppsWithAppPermissions.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">Third-Party Applications with Application Permissions <span class="red-flag-count">$($ThirdPartyAppsWithAppPermissions.Count)</span></h4>
                <p class="red-flag-desc">These third-party applications have application-level permissions which grant access without user context. Review these regularly to ensure they follow the principle of least privilege.</p>
                <div style="margin-top: 8px;">
                    <span class="red-flag-link" onclick="filterByThirdPartyWithAppPermissions()">Filter to show these apps</span>
                </div>
            </div>
"@

        }
        
        if ($NonActiveApps.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">Applications with No Recent Activity <span class="red-flag-count">$($NonActiveApps.Count)</span></h4>
                <p class="red-flag-desc">These applications show no sign-in activity. Consider reviewing and removing unused applications to reduce potential attack surface and improve security.</p>
                <div style="margin-top: 8px;">
                    <span class="red-flag-link" onclick="filterByInactiveApps()">Filter to show these apps</span>
                </div>
            </div>
"@

        }
        
        $html += " </div>`n"
    }

    $html += @"
        <div class="controls">
            <div class="search-box">
                <input type="text" id="searchInput" placeholder="Search applications...">
            </div>
            <div class="search-box">
                <input type="text" id="permissionSearchInput" placeholder="Search permissions...">
            </div>
            <select id="sourceFilter">
                <option value="all">All Sources</option>
                <option value="First Party">First Party Only</option>
                <option value="Third Party">Third Party Only</option>
            </select>
            <select id="scopeTypeFilter">
                <option value="all">All Permission Types</option>
                <option value="Delegated">Delegated</option>
                <option value="Application">Application</option>
            </select>
            <select id="activityFilter">
                <option value="all">All Activity</option>
                <option value="active">Has Activity</option>
                <option value="inactive">No Activity</option>
            </select>
            <select id="credentialFilter">
                <option value="all">All Credentials</option>
                <option value="any">Any Credentials</option>
                <option value="none">No Credentials</option>
                <option value="cert">Client Certificate</option>
                <option value="secret">Client Secret</option>
                <option value="both">Certificate + Secret</option>
            </select>
            <button onclick="exportCSV()">Export CSV</button>
            <button onclick="window.print()">Print Report</button>
        </div>
         
        <div class="table-container">
            <table id="appTable">
                <thead>
                    <tr>
                        <th class="sort-icon" onclick="sortTable(0)">Application Name</th>
                        <th class="sort-icon" onclick="sortTable(1)">App ID</th>
                        <th class="sort-icon" onclick="sortTable(2)">Source</th>
                        <th class="sort-icon" onclick="sortTable(3)">Permission Type</th>
                        <th>Permissions</th>
                        <th class="sort-icon" onclick="sortTable(5)">Last Delegated Sign-in</th>
                        <th class="sort-icon" onclick="sortTable(6)">Last Application Sign-in</th>
                        <th class="sort-icon" onclick="sortTable(7)">Last Sign-in Method</th>
                        <th>App Credentials</th>
                    </tr>
                </thead>
                <tbody>
                    $tableRows
                </tbody>
            </table>
        </div>
         
        <div class="footer">
            <p>Report generated using Entra Application Permissions Report Tool | © $(Get-Date -Format "yyyy") ourcloudnetwork.com</p>
        </div>
    </div>
     
    <script>
        // Search & Filter Functionality
        document.getElementById('searchInput').addEventListener('keyup', filterTable);
        document.getElementById('permissionSearchInput').addEventListener('keyup', filterTable);
        document.getElementById('sourceFilter').addEventListener('change', filterTable);
        document.getElementById('scopeTypeFilter').addEventListener('change', filterTable);
        document.getElementById('activityFilter').addEventListener('change', filterTable);
        document.getElementById('credentialFilter').addEventListener('change', filterTable);
         
        function filterTable() {
            const searchTerm = document.getElementById('searchInput').value.toLowerCase();
            const permissionSearchTerm = document.getElementById('permissionSearchInput').value.toLowerCase();
            const sourceFilter = document.getElementById('sourceFilter').value;
            const scopeFilter = document.getElementById('scopeTypeFilter').value;
            const activityFilter = document.getElementById('activityFilter').value;
            const credentialFilter = document.getElementById('credentialFilter').value;
            const rows = document.querySelectorAll('#appTable tbody tr');
             
            rows.forEach(row => {
                const appName = row.cells[0].textContent.toLowerCase();
                const appId = row.cells[1].textContent.toLowerCase();
                const source = row.cells[2].textContent;
                const scopeType = row.cells[3].textContent;
                const permissions = row.cells[4].textContent.toLowerCase();
                const delegatedActivity = row.cells[5].textContent;
                const applicationActivity = row.cells[6].textContent;
                const signInMethod = row.cells[7] ? row.cells[7].textContent : '';
                const credentials = row.cells[8] ? row.cells[8].textContent.toLowerCase() : '';
                 
                const matchesSearch = appName.includes(searchTerm) ||
                                     appId.includes(searchTerm) ||
                                     credentials.includes(searchTerm) ||
                                     signInMethod.toLowerCase().includes(searchTerm);
                                      
                const matchesPermissionSearch = permissionSearchTerm === '' ||
                                              permissions.includes(permissionSearchTerm);
                                               
                const matchesSource = sourceFilter === 'all' || source === sourceFilter;
                const matchesScopeFilter = scopeFilter === 'all' || scopeType === scopeFilter;
                 
                let matchesActivity = true;
                if (activityFilter === 'active') {
                    matchesActivity = !(delegatedActivity.includes('No activity') && applicationActivity.includes('No activity'));
                } else if (activityFilter === 'inactive') {
                    matchesActivity = delegatedActivity.includes('No activity') && applicationActivity.includes('No activity');
                }
                 
                // Process credential filter
                let matchesCredential = true;
                const hasCert = credentials.includes('client certificate');
                const hasSecret = credentials.includes('client secret');
                const hasAnyCredential = hasCert || hasSecret;
                 
                switch (credentialFilter) {
                    case 'any':
                        matchesCredential = hasAnyCredential;
                        break;
                    case 'none':
                        matchesCredential = credentials.includes('none');
                        break;
                    case 'cert':
                        matchesCredential = hasCert;
                        break;
                    case 'secret':
                        matchesCredential = hasSecret;
                        break;
                    case 'both':
                        matchesCredential = hasCert && hasSecret;
                        break;
                    default: // 'all'
                        matchesCredential = true;
                }
                 
                row.style.display = (matchesSearch && matchesPermissionSearch &&
                                    matchesSource && matchesScopeFilter &&
                                    matchesActivity && matchesCredential) ? '' : 'none';
            });
        }
         
        // Sorting Functionality - Updated with new column indices
        let currentSortCol = -1;
        let currentSortDir = 'asc';
         
        function sortTable(columnIndex) {
            const table = document.getElementById('appTable');
            const headers = table.querySelectorAll('th');
             
            // Reset all headers
            headers.forEach(header => {
                header.classList.remove('sort-asc', 'sort-desc');
                if (header.classList.contains('sort-icon')) {
                    header.classList.add('sort-icon');
                }
            });
             
            // Set sort direction
            if (currentSortCol === columnIndex) {
                currentSortDir = currentSortDir === 'asc' ? 'desc' : 'asc';
            } else {
                currentSortDir = 'asc';
            }
             
            // Update header class
            headers[columnIndex].classList.add(currentSortDir === 'asc' ? 'sort-asc' : 'sort-desc');
             
            currentSortCol = columnIndex;
             
            const rows = Array.from(table.querySelectorAll('tbody tr'));
             
            const sortedRows = rows.sort((a, b) => {
                let aVal = a.cells[columnIndex].textContent.trim();
                let bVal = b.cells[columnIndex].textContent.trim();
                 
                // Special handling for date columns
                if (columnIndex === 6 || columnIndex === 7) {
                    aVal = aVal.includes('No activity') ? '1900-01-01' : aVal;
                    bVal = bVal.includes('No activity') ? '1900-01-01' : bVal;
                }
                 
                const comparison = aVal.localeCompare(bVal, undefined, {numeric: true, sensitivity: 'base'});
                return currentSortDir === 'asc' ? comparison : -comparison;
            });
             
            // Remove existing rows
            rows.forEach(row => row.parentNode.removeChild(row));
             
            // Append sorted rows
            const tbody = table.querySelector('tbody');
            sortedRows.forEach(row => tbody.appendChild(row));
        }
         
        // CSV Export Functionality
        function exportCSV() {
            const table = document.getElementById('appTable');
            const rows = Array.from(table.querySelectorAll('tr'));
             
            // Create CSV content with BOM for Excel UTF-8 support
            let csvContent = [];
             
            // Process each row
            rows.forEach((row, i) => {
                // Skip hidden rows
                if (row.style.display === 'none') return;
                 
                const cells = Array.from(row.cells);
                const rowData = [];
                 
                // Process each cell
                cells.forEach(cell => {
                    let content = '';
                     
                    // For permissions cell or credentials cell (contains a list)
                    if (cell.classList.contains('permission-cell') || cell.classList.contains('credential-cell')) {
                        // Get all list items and join with semicolons
                        const items = Array.from(cell.querySelectorAll('li')).map(li => li.textContent.trim());
                        content = items.join('; ');
                         
                        // If no items were found, check for "None" text
                        if (items.length === 0 && cell.textContent.includes('None')) {
                            content = 'None';
                        }
                    } else {
                        // For regular cells, just get the text content
                        content = cell.textContent.trim();
                         
                        // Clean up warning icons and other special characters
                        content = content.replace('⚠️', '').trim();
                         
                        // For checkmark/x-mark cells, convert to Yes/No
                        if (content === '✓') content = 'Yes';
                        if (content === '✗') content = 'No';
                    }
                     
                    // Escape quotes with double quotes (CSV standard)
                    content = content.replace(/"/g, '""');
                    rowData.push('"' + content + '"');
                });
                 
                // Add row to CSV content
                csvContent.push(rowData.join(','));
            });
             
            // Create a BOM for UTF-8
            const BOM = '\uFEFF';
             
            // Join all rows with proper newlines and add BOM
            const csvString = BOM + csvContent.join('\r\n');
             
            // Create blob and download
            const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const link = document.createElement("a");
            link.setAttribute("href", url);
            link.setAttribute("download", "EntraAppReport_$(Get-Date -Format 'yyyy-MM-dd').csv");
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
        }
         
        // Red Flag filtering functions
        function filterByInsecureThirdPartyApps() {
            // Reset UI filters
            document.getElementById('searchInput').value = '';
            document.getElementById('permissionSearchInput').value = '';
            document.getElementById('sourceFilter').value = 'Third Party';
            document.getElementById('scopeTypeFilter').value = 'all';
            document.getElementById('activityFilter').value = 'all';
            document.getElementById('credentialFilter').value = 'any';
             
            // Apply filtering
            const rows = document.querySelectorAll('#appTable tbody tr');
            rows.forEach(row => {
                const source = row.cells[2].textContent;
                const signInMethod = row.cells[7].textContent;
                const credentials = row.cells[8].textContent;
                 
                // Show only third party apps with client certificate or client secret authentication
                const isThirdParty = source === 'Third Party';
                const hasInsecureAuth = signInMethod.includes('clientSecret') ||
                                       signInMethod.includes('certificate') ||
                                       signInMethod.includes('clientAssertion') ||
                                       credentials.includes('Client Certificate') ||
                                       credentials.includes('Client Secret');
                 
                row.style.display = (isThirdParty && hasInsecureAuth) ? '' : 'none';
            });
        }
         
        function filterByInsecureFirstPartyApps() {
            // Reset UI filters
            document.getElementById('searchInput').value = '';
            document.getElementById('permissionSearchInput').value = '';
            document.getElementById('sourceFilter').value = 'First Party';
            document.getElementById('scopeTypeFilter').value = 'all';
            document.getElementById('activityFilter').value = 'all';
            document.getElementById('credentialFilter').value = 'any';
             
            // Apply filtering
            const rows = document.querySelectorAll('#appTable tbody tr');
            rows.forEach(row => {
                const source = row.cells[2].textContent;
                const credentials = row.cells[8].textContent;
                 
                // Show only first party apps with client certificate or client secret credentials
                const isFirstParty = source === 'First Party';
                const hasInsecureAuth = credentials.includes('Client Certificate') ||
                                       credentials.includes('Client Secret');
                 
                row.style.display = (isFirstParty && hasInsecureAuth) ? '' : 'none';
            });
        }
         
        function filterByThirdPartyWithAppPermissions() {
            // Reset UI filters
            document.getElementById('searchInput').value = '';
            document.getElementById('permissionSearchInput').value = '';
            document.getElementById('sourceFilter').value = 'Third Party';
            document.getElementById('scopeTypeFilter').value = 'Application';
            document.getElementById('activityFilter').value = 'all';
            document.getElementById('credentialFilter').value = 'all';
             
            // The UI filters should handle this case automatically
            filterTable();
        }
         
        function filterByInactiveApps() {
            // Reset UI filters
            document.getElementById('searchInput').value = '';
            document.getElementById('permissionSearchInput').value = '';
            document.getElementById('sourceFilter').value = 'all';
            document.getElementById('scopeTypeFilter').value = 'all';
            document.getElementById('activityFilter').value = 'inactive';
            document.getElementById('credentialFilter').value = 'all';
             
            // The UI filters should handle this case automatically
            filterTable();
        }
         
        // Initialize sorting on application name column
        window.onload = function() {
            sortTable(0);
        };
    </script>
</body>
</html>
"@


    # Save the HTML file
    try {
        # If no output path is specified, use script root
        if ([string]::IsNullOrEmpty($OutputPath)) {
            $OutputPath = Join-Path $PSScriptRoot "EntraAppReport_$(Get-Date -Format 'yyyy-MM-dd_HH-mm').html"
        }
        
        # Ensure directory exists
        $directory = Split-Path -Path $OutputPath -Parent
        if (!(Test-Path $directory)) {
            New-Item -ItemType Directory -Path $directory -Force | Out-Null
        }
        
        $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force
        Write-Output "HTML Report saved to: $OutputPath"
        
        # Open the report in the default browser
        Start-Process $OutputPath
    }
    catch {
        Write-Error "Failed to save the HTML report: $_"
        return $null
    }
}

# Generate and open the HTML report after collecting the data
# Determine output path
Write-Progress -Activity "Generating Report" -Status "Creating HTML report" -PercentComplete 100
Write-Host "Generating HTML report..." -ForegroundColor Green
$reportOutputPath = $null
if ($outpath) {
    # Check if the provided path is a directory or a file
    if (Test-Path $outpath -PathType Container) {
        # It's a directory, append filename
        $reportOutputPath = Join-Path $outpath "EntraAppReport_$(Get-Date -Format 'yyyy-MM-dd_HH-mm').html"
    } else {
        # It's a file path or doesn't exist, use as is
        $reportOutputPath = $outpath
    }
}

Export-EntraAppHTMLReport -ReportData $AllfilteredPermissions -ReportTitle "Entra Application Permissions Report" -OutputPath $reportOutputPath

# Complete
Write-Progress -Activity "Generating Report" -Completed
Write-Host "Report generation complete!" -ForegroundColor Green