Invoke-EntraAppReport.ps1

#to do: Add filter for app with and without owners. Add a risk indicator for apps with no owners

<#PSScriptInfo
 
.VERSION 0.2.2
 
.GUID 175aa966-47dc-4a76-bbd1-b7ab11cd3079
 
.AUTHOR Daniel Bradley
 
.COMPANYNAME ourcloudnetwork.co.uk
 
.COPYRIGHT
 
.TAGS
    ourcloudnetwork
    Microsoft Entra
    Microsoft Graph
 
.LICENSEURI
    https://github.com/DanielBradley1/Invoke-EntraAppReport/blob/main/LICENSE
 
.PROJECTURI
    https://ourcloudnetwork.com/create-a-free-enterprise-app-permissions-report-in-microsoft-entra/
 
.EXTERNALMODULEDEPENDENCIES
    Microsoft.Graph.Authentication
 
.RELEASENOTES
    v0.1 - Initial release
    v0.2 - Added app registration owners
    v0.2.2 - Added risk indicators for insecure redirect URIs
#>


<#
.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
)

# 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,replyUrls&`$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, replyUrls, @{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,publicClient&`$count=true" -OutputType PSObject | Select -Expand Value
$credentials | ForEach-Object {
    $ownerdata = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/applications/$($_.id)/owners" -OutputType PSObject | Select -Expand Value
    $owner = $ownerdata.userPrincipalName
    $_ | Add-Member -MemberType NoteProperty -Name "Owners" -Value $owner
    $_ | Add-Member -MemberType NoteProperty -Name "RedirectUris" -Value $_.publicClient.RedirectUris
}


$AllfilteredPermissions | ForEach-Object {
    $app = $_
    $appcredentials = $credentials | Where-Object { $_.appId -eq $app.appId }
    $spdata = $spRoleAssignments | 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"
    }
    
    # Combine redirect URIs from both application and service principal
    $combinedRedirectUris = @()
    if ($null -ne $appcredentials.RedirectUris -and $appcredentials.RedirectUris.Count -gt 0) {
        $combinedRedirectUris += $appcredentials.RedirectUris
    }
    if ($null -ne $spdata.replyUrls -and $spdata.replyUrls.Count -gt 0) {
        $combinedRedirectUris += $spdata.replyUrls
    }
    # Remove duplicates and keep only unique URIs
    $uniqueRedirectUris = $combinedRedirectUris | Sort-Object -Unique
    
    $_ | Add-Member -MemberType NoteProperty -Name "App Credentials" -Value $credentialsToAdd
    $_ | Add-Member -MemberType NoteProperty -Name "Owners" -Value $appcredentials.Owners
    $_ | Add-Member -MemberType NoteProperty -Name "RedirectUris" -Value $uniqueRedirectUris
}

# 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
# First-party apps with no owners
$FirstPartyAppsWithNoOwners = $AllfilteredPermissions | Where { ($_.appOwnerOrganizationId -eq $organisationInfo.id) -and ([string]::IsNullOrEmpty($_.Owners) -or $_.Owners.Count -eq 0) } | Select DisplayName -Unique

# Applications with insecure redirect URIs
$AppsWithInsecureRedirectUris = $AllfilteredPermissions | Where-Object { 
    $hasInsecureUri = $false
    if ($null -ne $_.RedirectUris -and $_.RedirectUris.Count -gt 0) {
        foreach ($uri in $_.RedirectUris) {
            # Check for localhost patterns
            if ($uri -match "^https?://localhost" -or $uri -match "^https?://127\.0\.0\.1" -or $uri -match "^https?://\[::1\]") {
                $hasInsecureUri = $true
                break
            }
            # Check for wildcard domains (but not specific subdomains)
            if ($uri -match "^https?://\*\." -or $uri -match "://\*/" -or $uri -like "*//*") {
                $hasInsecureUri = $true
                break
            }
            # Check for Azure Web Apps (*.azurewebsites.net)
            if ($uri -match "^https?://.*\.azurewebsites\.net") {
                $hasInsecureUri = $true
                break
            }
            # Check for URL shorteners
            if ($uri -match "^https?://(bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|short\.link|tiny\.cc)") {
                $hasInsecureUri = $true
                break
            }
            # Check for insecure HTTP (non-HTTPS)
            if ($uri -match "^http://(?!localhost|127\.0\.0\.1|\[::1\])") {
                $hasInsecureUri = $true
                break
            }
        }
    }
    $hasInsecureUri
} | 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);
        }
        
        /* Expandable row styles */
        .expand-btn {
            background: none;
            border: none;
            cursor: pointer;
            padding: 4px 8px;
            border-radius: 4px;
            transition: all 0.2s ease;
            font-size: 14px;
            color: var(--primary-color);
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-width: 24px;
            height: 24px;
        }
        
        .expand-btn:hover {
            background-color: rgba(37, 99, 235, 0.1);
            transform: scale(1.1);
        }
        
        .expand-btn svg {
            transition: transform 0.2s ease;
            width: 16px;
            height: 16px;
        }
        
        .expand-btn.expanded svg {
            transform: rotate(90deg);
        }
        
        .expandable-row {
            cursor: pointer;
        }
        
        .expandable-row:hover {
            background-color: var(--table-hover);
        }
        
        .detail-row {
            display: none;
            background-color: #f8fafc;
            border-left: 3px solid var(--primary-color);
        }
        
        .detail-row.expanded {
            display: table-row;
        }
        
        .detail-row td {
            padding: 0;
            border-bottom: 1px solid var(--table-border);
        }
        
        .detail-content {
            padding: 20px;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 15px;
            background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
            border-radius: 8px;
            margin: 10px;
        }
        
        .detail-item {
            background: white;
            padding: 16px 18px;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
            border: 1px solid rgba(0, 0, 0, 0.05);
            transition: all 0.2s ease;
        }
        
        .detail-item:hover {
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
            transform: translateY(-1px);
        }
        
        .detail-label {
            font-weight: 600;
            color: #374151;
            font-size: 13px;
            margin-bottom: 6px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        
        .detail-value {
            color: #1f2937;
            font-size: 14px;
            line-height: 1.4;
        }
        
        .detail-value .permission-list,
        .detail-value ul {
            margin: 0;
            padding-left: 16px;
        }
        
        .detail-value .no-activity {
            color: #6b7280;
            font-style: italic;
        }
        .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;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .red-flag-heading-text {
            display: flex;
            align-items: center;
        }
        
        .minimize-btn {
            background: none;
            border: none;
            color: #777;
            cursor: pointer;
            font-size: 18px;
            padding: 0 5px;
            line-height: 1;
            transition: all 0.2s;
            margin-left: 10px;
        }
        
        .minimize-btn:hover {
            color: #CD0000;
            background: none;
        }
        
        .red-flag-content {
            transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
            max-height: 500px;
            opacity: 1;
            overflow: hidden;
        }
        
        .red-flag-content.collapsed {
            max-height: 0;
            opacity: 0;
            margin-top: 0;
            margin-bottom: 0;
        }
         
        .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 = ""
    $rowIndex = 0
    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>"
        }
        
        # Format Owners as bullet points similar to permissions
        $ownersList = ""
        if ($null -ne $item.Owners -and $item.Owners.Count -gt 0) {
            $ownersList = "<ul class='permission-list'>"
            foreach ($owner in $item.Owners) {
                $ownersList += "<li>$owner</li>"
            }
            $ownersList += "</ul>"
        }
        else {
            $ownersList = "<span class='no-activity'>None</span>"
        }
        
        # Format Redirect URIs as bullet points similar to permissions
        $redirectUrisList = ""
        if ($null -ne $item.RedirectUris -and $item.RedirectUris.Count -gt 0) {
            $redirectUrisList = "<ul class='permission-list'>"
            foreach ($uri in $item.RedirectUris) {
                $redirectUrisList += "<li>$uri</li>"
            }
            $redirectUrisList += "</ul>"
        }
        else {
            $redirectUrisList = "<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 expandable-row'"
            
            # Only add warning icon for third-party apps with application permissions
            if ($item.ScopeType -eq "Application") {
                $warningIcon = "⚠️ "
            }
        } else {
            $rowClass = "class='expandable-row'"
        }
        
        # Main row with core information
        $tableRows += @"
        <tr $rowClass data-row-index="$rowIndex">
            <td>
                <button class="expand-btn" onclick="toggleRow($rowIndex)" title="Expand details">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <polyline points="9,18 15,12 9,6"></polyline>
                    </svg>
                </button>
                $warningIcon$($item.DisplayName)
            </td>
            <td>$($item.appId)</td>
            <td>$appSource</td>
            <td>$($item.ScopeType)</td>
            <td class="permission-cell">$scopeList</td>
        </tr>
        <tr class="detail-row" id="detail-$rowIndex">
            <td colspan="5">
                <div class="detail-content">
                    <div class="detail-item">
                        <div class="detail-label">Last Delegated Sign-in</div>
                        <div class="detail-value">$delegatedLastSignIn</div>
                    </div>
                    <div class="detail-item">
                        <div class="detail-label">Last Application Sign-in</div>
                        <div class="detail-value">$applicationLastSignIn</div>
                    </div>
                    <div class="detail-item">
                        <div class="detail-label">Last Sign-in Method</div>
                        <div class="detail-value">$lastSignInType</div>
                    </div>
                    <div class="detail-item">
                        <div class="detail-label">App Credentials</div>
                        <div class="detail-value">$credentialsList</div>
                    </div>
                    <div class="detail-item">
                        <div class="detail-label">Owners</div>
                        <div class="detail-value">$ownersList</div>
                    </div>
                    <div class="detail-item">
                        <div class="detail-label">Redirect URIs</div>
                        <div class="detail-value">$redirectUrisList</div>
                    </div>
                </div>
            </td>
        </tr>
"@

        $rowIndex++
    }

    # 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 -or $FirstPartyAppsWithNoOwners.Count -gt 0 -or $AppsWithInsecureRedirectUris.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">
                    <span class="red-flag-heading-text">Third-Party Applications Using Insecure Authentication <span class="red-flag-count">$($InsecureThirdPartyApps.Count)</span></span>
                    <button class="minimize-btn" onclick="toggleRedFlag(this)" title="Minimize">−</button>
                </h4>
                <div class="red-flag-content">
                    <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>
            </div>
"@

        }
        
        if ($InsecureFirstPartyApps.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">
                    <span class="red-flag-heading-text">First-Party Applications Using Insecure Authentication <span class="red-flag-count">$($InsecureFirstPartyApps.Count)</span></span>
                    <button class="minimize-btn" onclick="toggleRedFlag(this)" title="Minimize">−</button>
                </h4>
                <div class="red-flag-content">
                    <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>
            </div>
"@

        }
        
        if ($ThirdPartyAppsWithAppPermissions.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">
                    <span class="red-flag-heading-text">Third-Party Applications with Application Permissions <span class="red-flag-count">$($ThirdPartyAppsWithAppPermissions.Count)</span></span>
                    <button class="minimize-btn" onclick="toggleRedFlag(this)" title="Minimize">−</button>
                </h4>
                <div class="red-flag-content">
                    <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>
            </div>
"@

        }
        
        if ($NonActiveApps.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">
                    <span class="red-flag-heading-text">Applications with No Recent Activity <span class="red-flag-count">$($NonActiveApps.Count)</span></span>
                    <button class="minimize-btn" onclick="toggleRedFlag(this)" title="Minimize">−</button>
                </h4>
                <div class="red-flag-content">
                    <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>
            </div>
"@

        }
        
        if ($FirstPartyAppsWithNoOwners.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">
                    <span class="red-flag-heading-text">First-Party Applications with No Owners <span class="red-flag-count">$($FirstPartyAppsWithNoOwners.Count)</span></span>
                    <button class="minimize-btn" onclick="toggleRedFlag(this)" title="Minimize">−</button>
                </h4>
                <div class="red-flag-content">
                    <p class="red-flag-desc">These first-party applications have no assigned owners. Applications without owners can become orphaned when team members leave and may pose governance risks.</p>
                    <div style="margin-top: 8px;">
                        <span class="red-flag-link" onclick="filterByFirstPartyAppsWithNoOwners()">Filter to show these apps</span>
                    </div>
                </div>
            </div>
"@

        }
        
        if ($AppsWithInsecureRedirectUris.Count -gt 0) {
            $html += @"
            <div class="red-flag-item">
                <h4 class="red-flag-heading">
                    <span class="red-flag-heading-text">Applications with Insecure Redirect URIs <span class="red-flag-count">$($AppsWithInsecureRedirectUris.Count)</span></span>
                    <button class="minimize-btn" onclick="toggleRedFlag(this)" title="Minimize">−</button>
                </h4>
                <div class="red-flag-content">
                    <p class="red-flag-desc">These applications have potentially insecure redirect URIs including localhost addresses, wildcard domains, Azure Web Apps, URL shorteners, or insecure HTTP redirects. These configurations may pose security risks and should be reviewed.</p>
                    <div style="margin-top: 8px;">
                        <span class="red-flag-link" onclick="filterByInsecureRedirectUris()">Filter to show these apps</span>
                    </div>
                </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>
            <select id="ownerFilter">
                <option value="all">All Owners</option>
                <option value="with">With Owners</option>
                <option value="without">No Owners</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>
                    </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);
        document.getElementById('ownerFilter').addEventListener('change', filterTable);
        
        // Row expansion functionality
        function toggleRow(rowIndex) {
            const detailRow = document.getElementById('detail-' + rowIndex);
            const expandBtn = document.querySelector('tr[data-row-index="' + rowIndex + '"] .expand-btn');
            
            if (detailRow.classList.contains('expanded')) {
                detailRow.classList.remove('expanded');
                expandBtn.classList.remove('expanded');
            } else {
                detailRow.classList.add('expanded');
                expandBtn.classList.add('expanded');
            }
        }
         
        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 ownerFilter = document.getElementById('ownerFilter').value;
            
            // Get all main rows (not detail rows)
            const rows = document.querySelectorAll('#appTable tbody tr.expandable-row');
             
            rows.forEach(row => {
                const rowIndex = row.getAttribute('data-row-index');
                const detailRow = document.getElementById('detail-' + rowIndex);
                
                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();
                
                // Get data from detail row for additional filtering with error handling
                let delegatedActivity = 'No activity';
                let applicationActivity = 'No activity';
                let signInMethod = 'Unknown';
                let credentials = 'none';
                let owners = 'none';
                let redirectUris = 'none';
                
                try {
                    const detailContent = detailRow.querySelector('.detail-content');
                    if (detailContent && detailContent.children.length >= 6) {
                        delegatedActivity = detailContent.children[0].querySelector('.detail-value').textContent;
                        applicationActivity = detailContent.children[1].querySelector('.detail-value').textContent;
                        signInMethod = detailContent.children[2].querySelector('.detail-value').textContent;
                        credentials = detailContent.children[3].querySelector('.detail-value').textContent.toLowerCase();
                        owners = detailContent.children[4].querySelector('.detail-value').textContent.toLowerCase();
                        redirectUris = detailContent.children[5].querySelector('.detail-value').textContent.toLowerCase();
                    }
                } catch (e) {
                    console.log('Error accessing detail content for row ' + rowIndex + ':', e);
                }
                 
                const matchesSearch = appName.includes(searchTerm) ||
                                     appId.includes(searchTerm) ||
                                     credentials.includes(searchTerm) ||
                                     signInMethod.toLowerCase().includes(searchTerm) ||
                                     redirectUris.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;
                }
                
                // Process owner filter
                let matchesOwner = true;
                const hasOwners = !owners.includes('none');
                
                switch (ownerFilter) {
                    case 'with':
                        matchesOwner = hasOwners;
                        break;
                    case 'without':
                        matchesOwner = !hasOwners;
                        break;
                    default: // 'all'
                        matchesOwner = true;
                }
                
                const shouldShow = (matchesSearch && matchesPermissionSearch &&
                                  matchesSource && matchesScopeFilter &&
                                  matchesActivity && matchesCredential &&
                                  matchesOwner);
                
                row.style.display = shouldShow ? '' : 'none';
                detailRow.style.display = shouldShow ? '' : 'none';
                
                // Collapse detail row if main row is hidden
                if (!shouldShow && detailRow.classList.contains('expanded')) {
                    detailRow.classList.remove('expanded');
                    const expandBtn = row.querySelector('.expand-btn');
                    if (expandBtn) {
                        expandBtn.classList.remove('expanded');
                    }
                }
            });
        }
         
        // Sorting Functionality - Updated for expandable rows
        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;
             
            // Get only main rows (not detail rows) for sorting
            const mainRows = Array.from(table.querySelectorAll('tbody tr.expandable-row'));
             
            const sortedRowPairs = mainRows.map(row => {
                const rowIndex = row.getAttribute('data-row-index');
                const detailRow = document.getElementById('detail-' + rowIndex);
                return { main: row, detail: detailRow };
            }).sort((a, b) => {
                let aVal = a.main.cells[columnIndex].textContent.trim();
                let bVal = b.main.cells[columnIndex].textContent.trim();
                
                // Clean up app name for sorting (remove warning icons)
                if (columnIndex === 0) {
                    aVal = aVal.replace('⚠️', '').trim();
                    bVal = bVal.replace('⚠️', '').trim();
                }
                 
                const comparison = aVal.localeCompare(bVal, undefined, {numeric: true, sensitivity: 'base'});
                return currentSortDir === 'asc' ? comparison : -comparison;
            });
             
            // Remove all existing rows
            const tbody = table.querySelector('tbody');
            while (tbody.firstChild) {
                tbody.removeChild(tbody.firstChild);
            }
             
            // Append sorted row pairs in order
            sortedRowPairs.forEach(pair => {
                tbody.appendChild(pair.main);
                tbody.appendChild(pair.detail);
            });
        }
         
        // CSV Export Functionality - Updated for expandable rows
        function exportCSV() {
            const table = document.getElementById('appTable');
            
            // Create CSV content with BOM for Excel UTF-8 support
            let csvContent = [];
            
            // Add header row
            const headers = ['Application Name', 'App ID', 'Source', 'Permission Type', 'Permissions',
                           'Last Delegated Sign-in', 'Last Application Sign-in', 'Last Sign-in Method',
                           'App Credentials', 'Owners', 'Redirect URIs'];
            csvContent.push(headers.map(h => '"' + h + '"').join(','));
            
            // Get all main rows (not detail rows)
            const mainRows = Array.from(table.querySelectorAll('tbody tr.expandable-row'));
             
            mainRows.forEach(row => {
                // Skip hidden rows
                if (row.style.display === 'none') return;
                
                const rowIndex = row.getAttribute('data-row-index');
                const detailRow = document.getElementById('detail-' + rowIndex);
                const detailContent = detailRow.querySelector('.detail-content');
                
                const rowData = [];
                
                // Get main row data
                for (let i = 0; i < 5; i++) {
                    let content = '';
                    const cell = row.cells[i];
                    
                    if (i === 0) {
                        // Application name - remove expand button and clean up
                        content = cell.textContent.replace('⚠️', '').trim();
                        // Remove the expand button text/content
                        const expandBtn = cell.querySelector('.expand-btn');
                        if (expandBtn) {
                            content = content.replace(expandBtn.textContent || '', '').trim();
                        }
                    } else if (i === 4) {
                        // Permissions cell - handle list items
                        const items = Array.from(cell.querySelectorAll('li')).map(li => li.textContent.trim());
                        content = items.length > 0 ? items.join('; ') :
                                 (cell.textContent.includes('None') ? 'None' : cell.textContent.trim());
                    } else {
                        content = cell.textContent.trim();
                    }
                    
                    // Escape quotes with double quotes (CSV standard)
                    content = content.replace(/"/g, '""');
                    rowData.push('"' + content + '"');
                }
                
                // Get detail row data
                const detailItems = detailContent.querySelectorAll('.detail-item .detail-value');
                detailItems.forEach(item => {
                    let content = '';
                    
                    // Handle list items in detail data
                    const items = Array.from(item.querySelectorAll('li')).map(li => li.textContent.trim());
                    if (items.length > 0) {
                        content = items.join('; ');
                    } else {
                        content = item.textContent.trim();
                    }
                    
                    // 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 - Updated for expandable rows
        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';
            document.getElementById('ownerFilter').value = 'all';
             
            // Apply filtering
            const rows = document.querySelectorAll('#appTable tbody tr.expandable-row');
            rows.forEach(row => {
                const rowIndex = row.getAttribute('data-row-index');
                const detailRow = document.getElementById('detail-' + rowIndex);
                
                const source = row.cells[2].textContent;
                let signInMethod = 'Unknown';
                let credentials = 'none';
                
                try {
                    const detailContent = detailRow.querySelector('.detail-content');
                    if (detailContent && detailContent.children.length >= 6) {
                        signInMethod = detailContent.children[2].querySelector('.detail-value').textContent;
                        credentials = detailContent.children[3].querySelector('.detail-value').textContent;
                    }
                } catch (e) {
                    console.log('Error accessing detail content for red flag filter:', e);
                }
                 
                // 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');
                 
                const shouldShow = isThirdParty && hasInsecureAuth;
                row.style.display = shouldShow ? '' : 'none';
                detailRow.style.display = shouldShow ? '' : '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';
            document.getElementById('ownerFilter').value = 'all';
             
            // Apply filtering
            const rows = document.querySelectorAll('#appTable tbody tr.expandable-row');
            rows.forEach(row => {
                const rowIndex = row.getAttribute('data-row-index');
                const detailRow = document.getElementById('detail-' + rowIndex);
                
                const source = row.cells[2].textContent;
                let credentials = 'none';
                
                try {
                    const detailContent = detailRow.querySelector('.detail-content');
                    if (detailContent && detailContent.children.length >= 6) {
                        credentials = detailContent.children[3].querySelector('.detail-value').textContent;
                    }
                } catch (e) {
                    console.log('Error accessing detail content for red flag filter:', e);
                }
                 
                // 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');
                 
                const shouldShow = isFirstParty && hasInsecureAuth;
                row.style.display = shouldShow ? '' : 'none';
                detailRow.style.display = shouldShow ? '' : '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';
            document.getElementById('ownerFilter').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';
            document.getElementById('ownerFilter').value = 'all';
             
            // The UI filters should handle this case automatically
            filterTable();
        }
        
        function filterByFirstPartyAppsWithNoOwners() {
            // 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 = 'all';
            document.getElementById('ownerFilter').value = 'without';
            
            // The UI filters should handle this case automatically
            filterTable();
        }
        
        function filterByInsecureRedirectUris() {
            // Reset most filters but use search to find apps with insecure redirect URIs
            document.getElementById('searchInput').value = '';
            document.getElementById('permissionSearchInput').value = '';
            document.getElementById('sourceFilter').value = 'all';
            document.getElementById('scopeTypeFilter').value = 'all';
            document.getElementById('activityFilter').value = 'all';
            document.getElementById('credentialFilter').value = 'all';
            document.getElementById('ownerFilter').value = 'all';
            
            // Use custom filtering logic to show only apps with insecure redirect URIs
            const rows = document.querySelectorAll('#appTable tbody tr.expandable-row');
            
            rows.forEach(row => {
                const rowIndex = row.getAttribute('data-row-index');
                const detailRow = document.getElementById('detail-' + rowIndex);
                let hasInsecureUri = false;
                
                try {
                    const detailContent = detailRow.querySelector('.detail-content');
                    if (detailContent && detailContent.children.length >= 6) {
                        const redirectUrisElement = detailContent.children[5].querySelector('.detail-value');
                        const redirectUrisText = redirectUrisElement.textContent;
                        
                        // Split URIs and check each one with precise pattern matching
                        const uris = redirectUrisText.split(',').map(uri => uri.trim()).filter(uri => uri && uri !== 'None');
                        
                        for (const uri of uris) {
                            // Check for localhost patterns
                            if (/^https?:\/\/localhost/.test(uri) || /^https?:\/\/127\.0\.0\.1/.test(uri) || /^https?:\/\/\[::1\]/.test(uri)) {
                                hasInsecureUri = true;
                                break;
                            }
                            // Check for wildcard domains (but not specific subdomains)
                            if (/^https?:\/\/\*\./.test(uri) || /:\/\/\*\//.test(uri) || uri.includes('//*')) {
                                hasInsecureUri = true;
                                break;
                            }
                            // Check for Azure Web Apps (*.azurewebsites.net)
                            if (/^https?:\/\/.*\.azurewebsites\.net/.test(uri)) {
                                hasInsecureUri = true;
                                break;
                            }
                            // Check for URL shorteners
                            if (/^https?:\/\/(bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|short\.link|tiny\.cc)/.test(uri)) {
                                hasInsecureUri = true;
                                break;
                            }
                            // Check for insecure HTTP (non-HTTPS) excluding localhost
                            if (/^http:\/\/(?!localhost|127\.0\.0\.1|\[::1\])/.test(uri)) {
                                hasInsecureUri = true;
                                break;
                            }
                        }
                    }
                } catch (e) {
                    console.log('Error checking redirect URIs for row ' + rowIndex + ':', e);
                }
                
                row.style.display = hasInsecureUri ? '' : 'none';
                detailRow.style.display = hasInsecureUri ? '' : 'none';
                
                // Collapse detail row if main row is hidden
                if (!hasInsecureUri && detailRow.classList.contains('expanded')) {
                    detailRow.classList.remove('expanded');
                    const expandBtn = row.querySelector('.expand-btn');
                    if (expandBtn) {
                        expandBtn.classList.remove('expanded');
                    }
                }
            });
        }
         
        // Initialize sorting on application name column
        window.onload = function() {
            sortTable(0);
        };
        
        // Function to toggle red flag sections
        function toggleRedFlag(button) {
            const content = button.closest('.red-flag-item').querySelector('.red-flag-content');
            content.classList.toggle('collapsed');
            
            if (content.classList.contains('collapsed')) {
                button.textContent = '+';
                button.title = 'Expand';
            } else {
                button.textContent = '−';
                button.title = 'Minimize';
            }
        }
        
        // Make toggleRow available globally
        window.toggleRow = toggleRow;
    </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