This came from a requirement to see which of the Enterprise Applications and App Registrations have been configured and which have been left to "ruin" with nothing setup on them, commonly know as phantom records which are there but do nothing.
The terminology may not work outside Entra so Enterprise Application are usual SAML based authentication then you have Application Registrations which are an OpenID based authentication - this is the type of the client secret and API permission (or Microsoft Graph based)
This is the plan:
- Return a list of Enterprise Applications that have SAML configured
- Return the Owner for that SAML application
- Return the recent sign-in history for that SAML application
- Return the IP address from where the user used this SAML application
- Return a list of App Registrations that have client secrets configured
- Return the Owner for that OpenID application
- Return the recent sign-in history for that OpenID application
- Return the IP address from where the user used this OpenID application
- If there are no sign-ins report "No sign-ins recorded in the last 30 days"
- Output that to a html file with two tabs, once for SAML and one for OpenID
- Display the data in an easy to read format that is easy to read.
- Produce that report in the folder from where the script is run
That report will look like this, with this example in the Enterprise Applications:
Script : FederationAuditing.ps1
Pre-Flight Requirements
Note : You need to have an App Registration setup for this to work, for the basics on that you can follow the article here
You will then require the following application permissions for this script to work, how to use these permissions will be in the link above:
Microsoft.Graph/AuditLog.Read.All
Microsoft.Graph/Application.Read.All
Microsoft.Graph/Directory.Read.All
Obtain the data from Entra
This is where we extract the information from Entra, depending on the amount of applications this may take a while to run, when you start the script get a coffee or go to lunch.
# Import required modules
Import-Module Microsoft.Graph.Applications
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Identity.SignIns
# Add verbose output
$VerbosePreference = "Continue"
# App registration details
$tenantId = "<tenant_id>"
$clientId = "<client_id>"
$clientSecret = "<secret>"
Write-Verbose "Creating credentials..."
$clientSecureString = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
$clientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $clientId, $clientSecureString
Write-Verbose "Connecting to Microsoft Graph..."
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $clientSecretCredential
$results = @{
AppRegistrations = @()
EnterpriseApps = @()
}
Write-Verbose "Getting application registrations..."
$appRegistrations = Get-MgApplication -All
Write-Verbose "Getting enterprise applications..."
$enterpriseApps = Get-MgServicePrincipal -All | Where-Object {
$_.Tags -contains "WindowsAzureActiveDirectoryIntegratedApp"
}
# Get sign-in logs for the last 30 days
$thirtyDaysAgo = (Get-Date).AddDays(-30)
$thirtyDaysAgoFormatted = $thirtyDaysAgo.ToString("yyyy-MM-dd")
Write-Verbose "Processing Application Registrations..."
# Process Application Registrations
foreach ($app in $appRegistrations) {
Write-Verbose "Processing app: $($app.DisplayName)"
$signInLogs = Get-MgAuditLogSignIn -Filter "appId eq '$($app.AppId)' and createdDateTime ge $thirtyDaysAgoFormatted" -Top 30 -OrderBy "createdDateTime desc"
Write-Verbose "Getting owners for: $($app.DisplayName)"
# Get app owner
$owners = Get-MgApplicationOwner -ApplicationId $app.Id
$ownerDetails = foreach ($owner in $owners) {
Get-MgUser -UserId $owner.Id | Select-Object DisplayName, UserPrincipalName
}
if ($app.Web.FederatedIdentityCredentials) {
Write-Verbose "SAML enabled for: $($app.DisplayName)"
$results.AppRegistrations += [PSCustomObject]@{
AppName = $app.DisplayName
AppId = $app.AppId
AuthType = "SAML Enabled"
Owners = $ownerDetails
SignIns = $signInLogs | Select-Object @{
Name='UserDisplayName'; Expression={$_.UserDisplayName}
}, @{
Name='UserPrincipalName'; Expression={$_.UserPrincipalName}
}, @{
Name='IPAddress'; Expression={$_.IpAddress}
}, @{
Name='LastSignIn'; Expression={$_.CreatedDateTime}
}
LastUsed = if ($signInLogs) { ($signInLogs | Select-Object -First 1).CreatedDateTime } else { $null }
}
}
if ($app.Web.ImplicitGrantSettings.EnableIdTokenIssuance) {
$hasClientSecret = Get-MgApplication -ApplicationId $app.Id | Select-Object -ExpandProperty PasswordCredentials
if ($hasClientSecret) {
Write-Verbose "Client Secret configured for: $($app.DisplayName)"
$results.AppRegistrations += [PSCustomObject]@{
AppName = $app.DisplayName
AppId = $app.AppId
AuthType = "Client Secret Configured"
Owners = $ownerDetails
SignIns = $signInLogs | Select-Object @{
Name='UserDisplayName'; Expression={$_.UserDisplayName}
}, @{
Name='UserPrincipalName'; Expression={$_.UserPrincipalName}
}, @{
Name='IPAddress'; Expression={$_.IpAddress}
}, @{
Name='LastSignIn'; Expression={$_.CreatedDateTime}
}
LastUsed = if ($signInLogs) { ($signInLogs | Select-Object -First 1).CreatedDateTime } else { $null }
}
}
}
}
Write-Verbose "Processing Enterprise Applications..."
# Process Enterprise Applications
foreach ($app in $enterpriseApps) {
Write-Verbose "Processing enterprise app: $($app.DisplayName)"
$signInLogs = Get-MgAuditLogSignIn -Filter "appId eq '$($app.AppId)' and createdDateTime ge $thirtyDaysAgoFormatted" -Top 30 -OrderBy "createdDateTime desc"
Write-Verbose "Getting owners for enterprise app: $($app.DisplayName)"
# Get app owner
$owners = Get-MgServicePrincipalOwner -ServicePrincipalId $app.Id
$ownerDetails = foreach ($owner in $owners) {
Get-MgUser -UserId $owner.Id | Select-Object DisplayName, UserPrincipalName
}
# Check for SAML configuration
if ($app.PreferredSingleSignOnMode -eq "saml") {
Write-Verbose "SAML enabled for enterprise app: $($app.DisplayName)"
$results.EnterpriseApps += [PSCustomObject]@{
AppName = $app.DisplayName
AppId = $app.AppId
AuthType = "SAML Enabled"
Owners = $ownerDetails
SignIns = $signInLogs | Select-Object @{
Name='UserDisplayName'; Expression={$_.UserDisplayName}
}, @{
Name='UserPrincipalName'; Expression={$_.UserPrincipalName}
}, @{
Name='IPAddress'; Expression={$_.IpAddress}
}, @{
Name='LastSignIn'; Expression={$_.CreatedDateTime}
}
LastUsed = if ($signInLogs) { ($signInLogs | Select-Object -First 1).CreatedDateTime } else { $null }
}
}
# Check for OIDC configuration
if ($app.PreferredSingleSignOnMode -eq "oidc") {
Write-Verbose "OIDC configured for enterprise app: $($app.DisplayName)"
$results.EnterpriseApps += [PSCustomObject]@{
AppName = $app.DisplayName
AppId = $app.AppId
AuthType = "OIDC Configured"
Owners = $ownerDetails
SignIns = $signInLogs | Select-Object @{
Name='UserDisplayName'; Expression={$_.UserDisplayName}
}, @{
Name='UserPrincipalName'; Expression={$_.UserPrincipalName}
}, @{
Name='IPAddress'; Expression={$_.IpAddress}
}, @{
Name='LastSignIn'; Expression={$_.CreatedDateTime}
}
LastUsed = if ($signInLogs) { ($signInLogs | Select-Object -First 1).CreatedDateTime } else { $null }
}
}
}
Write-Verbose "Generating HTML report..."
# Function to generate HTML for an application section
function Get-AppHtml {
param (
[Parameter(Mandatory=$true)]
[PSCustomObject]$App
)
$ownersList = ($App.Owners | ForEach-Object { $_.DisplayName }) -join ", "
$lastUsed = if ($App.LastUsed) {
[DateTime]$App.LastUsed | Get-Date -Format "MMM dd, yyyy HH:mm"
} else {
"Never"
}
$authTypeClass = switch ($App.AuthType) {
"SAML Enabled" { "auth-saml" }
"Client Secret Configured" { "auth-secret" }
"OIDC Configured" { "auth-oidc" }
default { "auth-other" }
}
$signInRows = if ($App.SignIns) {
$App.SignIns | ForEach-Object {
$signInDate = [DateTime]$_.LastSignIn | Get-Date -Format "MMM dd, yyyy HH:mm"
@"
<tr>
<td>$($_.UserDisplayName)</td>
<td>$($_.UserPrincipalName)</td>
<td>$($_.IPAddress)</td>
<td>$signInDate</td>
</tr>
"@
}
} else {
"<tr><td colspan='4'>No sign-ins recorded in the last 30 days</td></tr>"
}
return @"
<div class="app-card">
<div class="app-header">
<div class="app-title">
<h3>$($App.AppName)</h3>
<span class="auth-type $authTypeClass">$($App.AuthType)</span>
</div>
<div class="app-meta">
<span>Last Used: $lastUsed</span>
<span>Owners: $ownersList</span>
</div>
</div>
<table>
<thead>
<tr>
<th>User</th>
<th>Email</th>
<th>IP Address</th>
<th>Last Sign In</th>
</tr>
</thead>
<tbody>
$signInRows
</tbody>
</table>
</div>
"@
}
# Generate HTML content
$htmlContent = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Application Audit Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #2d3748;
margin-bottom: 1.5rem;
}
.tab-container {
margin-top: 2rem;
}
.tab-content {
display: none;
padding: 20px 0;
}
.tab-content.active {
display: block;
}
.tabs {
border-bottom: 2px solid #e2e8f0;
margin-bottom: 1rem;
}
.tab-button {
background: none;
border: none;
padding: 10px 20px;
font-size: 1rem;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.tab-button.active {
border-bottom: 2px solid #4a5568;
color: #4a5568;
}
.app-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 1.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.app-title {
display: flex;
align-items: center;
gap: 1rem;
}
.auth-type {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
color: white;
}
.auth-saml {
background-color: #4299e1;
}
.auth-secret {
background-color: #48bb78;
}
.auth-oidc {
background-color: #805ad5;
}
.auth-other {
background-color: #718096;
}
.app-meta {
display: flex;
gap: 1rem;
color: #666;
font-size: 0.9rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
th {
background-color: #f7fafc;
font-weight: 600;
}
.summary {
background: white;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
border: 1px solid #e2e8f0;
}
.summary h4 {
margin: 0 0 0.5rem 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Application Audit Report</h1>
<div class="tabs">
<button class="tab-button active" onclick="showTab('appregistrations')">Application Registrations</button>
<button class="tab-button" onclick="showTab('enterpriseapps')">Enterprise Applications</button>
</div>
<div id="appregistrations" class="tab-content active">
<div class="summary">
<h4>Application Registrations Summary</h4>
Total: $($results.AppRegistrations.Count)
| SAML Enabled: $($results.AppRegistrations | Where-Object { $_.AuthType -eq 'SAML Enabled' } | Measure-Object | Select-Object -ExpandProperty Count)
| Client Secret Configured: $($results.AppRegistrations | Where-Object { $_.AuthType -eq 'Client Secret Configured' } | Measure-Object | Select-Object -ExpandProperty Count)
</div>
$($results.AppRegistrations | ForEach-Object { Get-AppHtml $_ })
</div>
<div id="enterpriseapps" class="tab-content">
<div class="summary">
<h4>Enterprise Applications Summary</h4>
Total: $($results.EnterpriseApps.Count)
| SAML Enabled: $($results.EnterpriseApps | Where-Object { $_.AuthType -eq 'SAML Enabled' } | Measure-Object | Select-Object -ExpandProperty Count)
| OIDC Configured: $($results.EnterpriseApps | Where-Object { $_.AuthType -eq 'OIDC Configured' } | Measure-Object | Select-Object -ExpandProperty Count)
</div>
$($results.EnterpriseApps | ForEach-Object { Get-AppHtml $_ })
</div>
</div>
<script>
function showTab(tabId) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Remove active class from all buttons
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
// Show selected tab content
document.getElementById(tabId).classList.add('active');
// Add active class to clicked button
event.target.classList.add('active');
}
</script>
</body>
</html>
"@
Write-Verbose "Saving HTML report..."
# Save the HTML file
$htmlContent | Out-File "app_audit_report.html" -Encoding UTF8
Write-Verbose "Disconnecting from Microsoft Graph..."
Disconnect-MgGraph
Write-Host "Report has been generated as 'app_audit_report.html'"
Overview Report
We now have a report called app_audit_report.html we now need a list of applications that this report has found without all the additional data, so this script will list all the applications one by one with no data, but this report is driven from the html file created earlier.
This script, using the same tabs will list the names of the applications without any other data as you can see below:
Note : This requires the app_audit_report.html from the previous script, without this the script cannot run!
This script, using the same tabs will list the names of the applications without any other data as you can see below:
Note : This requires the app_audit_report.html from the previous script, without this the script cannot run!
Script : OverviewReport.ps1
# Load the HTML content
$htmlContent = Get-Content -Path "app_audit_report.html" -Raw
# Using regex to extract applications and their types
$results = @{
SAML = @()
OpenID = @()
}
# Pattern to match application entries
$pattern = '(?s)<h3>(.*?)</h3>.*?<span class="auth-type.*?">(.*?)</span>'
$matches = [regex]::Matches($htmlContent, $pattern)
foreach($match in $matches) {
$appName = $match.Groups[1].Value.Trim()
$authType = $match.Groups[2].Value.Trim()
# Categorize based on auth type
if ($authType -eq "SAML Enabled") {
$results.SAML += [PSCustomObject]@{
Name = $appName
Type = "SAML"
}
}
elseif ($authType -eq "Client Secret Configured" -or $authType -eq "OIDC Configured") {
$results.OpenID += [PSCustomObject]@{
Name = $appName
Type = "OpenID"
}
}
}
# Generate new HTML report
$newHtmlContent = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Federation Overview Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.tab-content {
display: none;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.tab-content.active {
display: block;
}
.tabs {
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab-button.active {
border-bottom: 2px solid #4a5568;
color: #4a5568;
}
.app-list {
list-style: none;
padding: 0;
}
.app-item {
padding: 12px;
border-bottom: 1px solid #edf2f7;
}
.app-item:last-child {
border-bottom: none;
}
.summary {
margin-bottom: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 6px;
}
</style>
</head>
<body>
<div class="container">
<h1>Federation Overview Report</h1>
<div class="tabs">
<button class="tab-button active" onclick="showTab('saml')">SAML Applications</button>
<button class="tab-button" onclick="showTab('openid')">OpenID Applications</button>
</div>
<div id="saml" class="tab-content active">
<div class="summary">
Total SAML Applications: $($results.SAML.Count)
</div>
<ul class="app-list">
$(foreach ($app in $results.SAML) {
"<li class='app-item'>$($app.Name)</li>"
})
</ul>
</div>
<div id="openid" class="tab-content">
<div class="summary">
Total OpenID Applications: $($results.OpenID.Count)
</div>
<ul class="app-list">
$(foreach ($app in $results.OpenID) {
"<li class='app-item'>$($app.Name)</li>"
})
</ul>
</div>
</div>
<script>
function showTab(tabId) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
document.querySelector(`[onclick="showTab('${tabId}')"]`).classList.add('active');
}
</script>
</body>
</html>
"@
# Save the new report
$newHtmlContent | Out-File "application_auth_report.html" -Encoding UTF8
Write-Host "New report has been generated as 'application_auth_report.html'"
Obtain a list of "all" Enterprise Applications/App Registrations
Well, this is the more interesting one from a audit point of view, how many applications do you have that "do nothing" that are not setup with no "single sign on" and no "client secrets" - housekeeping on this is usually not completed very well.
If you would like to know then first we need a list off all the application which includes SAML/OpenID regardless of their state this needs to be outputted for this example to a subfolder called "Exports" then the simple text files need to be called a relevant name:
This file will contain a list of all the applications you have, for both SAML and OpenID and it will just take the name of the application (as said earlier), for the script below you can use the same OpenID details from the earlier script.
This file will contain a list of all the applications you have, for both SAML and OpenID and it will just take the name of the application (as said earlier), for the script below you can use the same OpenID details from the earlier script.
Script : ExtractFullList.ps1
# Import required modules
Import-Module Microsoft.Graph.Applications
Import-Module Microsoft.Graph.Authentication
# App registration details
$tenantId = "<tenant_id>"
$clientId = "<client_id>"
$clientSecret = "<secret>"
# Create credential
$clientSecureString = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
$clientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $clientId, $clientSecureString
# Connect to Graph
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $clientSecretCredential
# Create Exports folder if it doesn't exist
if (-not (Test-Path "Exports")) {
New-Item -ItemType Directory -Path "Exports"
Write-Verbose "Created Exports directory"
}
# Arrays to store application names
$samlApps = @()
$openIDApps = @()
# Get enterprise applications (SAML)
$enterpriseApps = Get-MgServicePrincipal -All | Where-Object {
$_.PreferredSingleSignOnMode -eq "saml"
}
$samlApps = $enterpriseApps | Select-Object -ExpandProperty DisplayName
# Get application registrations with client secrets
$appRegistrations = Get-MgApplication -All
foreach ($app in $appRegistrations) {
$appInfo = Get-MgApplication -ApplicationId $app.Id
if ($appInfo.Web.ImplicitGrantSettings.EnableIdTokenIssuance) {
$openIDApps += $app.DisplayName
}
}
# Export to text files
$samlApps | Out-File "Exports\SAML.txt"
$openIDApps | Out-File "Exports\OpenID.txt"
Write-Host "Exported SAML applications to Exports\SAML.txt"
Write-Host "Exported OpenID applications to Exports\OpenID.txt"
Write-Host "SAML Applications: $($samlApps.Count)"
Write-Host "OpenID Applications: $($openIDApps.Count)"
Disconnect-MgGraph
Produce the "un-configured" report
Now we have the exported list of applications we now need to see which applications are not confirmed in a nice html document that will list all the applications where applicable, this will use the data from the application_auth_report.html report which we obtained from the OverviewReport.ps1 script.
This will then produce a report saying which application are not configured for both types of applications, in this example we are all good for SAML applications:
However we are not so tidy with the OpenID based applications as you can see below:
However we are not so tidy with the OpenID based applications as you can see below:
Script : UnconfiguredFederation.ps1
# Load the HTML content
$htmlContent = Get-Content -Path "application_auth_report.html" -Raw
# Initialize arrays for configured apps
$configuredSamlApps = @()
$configuredOpenIDApps = @()
# Extract SAML apps
if ($htmlContent -match '(?s)<div id="saml".*?<ul class="app-list">(.*?)</ul>') {
$samlList = $matches[1]
$samlMatches = [regex]::Matches($samlList, "<li class='app-item'>(.*?)</li>")
$configuredSamlApps = $samlMatches | ForEach-Object { $_.Groups[1].Value.Trim() }
}
# Extract OpenID apps
if ($htmlContent -match '(?s)<div id="openid".*?<ul class="app-list">(.*?)</ul>') {
$openidList = $matches[1]
$openidMatches = [regex]::Matches($openidList, "<li class='app-item'>(.*?)</li>")
$configuredOpenIDApps = $openidMatches | ForEach-Object { $_.Groups[1].Value.Trim() }
}
Write-Host "Found configured apps:"
Write-Host "SAML: $($configuredSamlApps.Count)"
Write-Host "OpenID: $($configuredOpenIDApps.Count)"
# Get all apps from export files and ensure proper trimming
Write-Host "`nReading export files..."
$allSamlApps = Get-Content "Exports\SAML.txt" |
Where-Object { $_ -match '\S' } |
ForEach-Object { $_.Trim() }
$allOpenIDApps = Get-Content "Exports\OpenID.txt" |
Where-Object { $_ -match '\S' } |
ForEach-Object { $_.Trim() }
Write-Host "Found in exports:"
Write-Host "SAML: $($allSamlApps.Count)"
Write-Host "OpenID: $($allOpenIDApps.Count)"
# Find unconfigured apps (apps in exports that are NOT in the configured list)
$unconfiguredSaml = $allSamlApps | Where-Object { $configuredSamlApps -notcontains $_ }
$unconfiguredOpenID = $allOpenIDApps | Where-Object { $configuredOpenIDApps -notcontains $_ }
Write-Host "`nFound unconfigured apps:"
Write-Host "SAML: $($unconfiguredSaml.Count)"
Write-Host "OpenID: $($unconfiguredOpenID.Count)"
# Generate new HTML report
$newHtmlContent = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Unconfigured Federated Applications</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.tab-content {
display: none;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.tab-content.active {
display: block;
}
.tabs {
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab-button.active {
border-bottom: 2px solid #4a5568;
color: #4a5568;
}
.app-list {
list-style: none;
padding: 0;
}
.app-item {
padding: 12px;
border-bottom: 1px solid #edf2f7;
color: #e53e3e;
}
.app-item:last-child {
border-bottom: none;
}
.summary {
margin-bottom: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 6px;
}
</style>
</head>
<body>
<div class="container">
<h1>Unconfigured Federated Applications</h1>
<div class="tabs">
<button class="tab-button active" onclick="showTab('saml')">Unconfigured SAML Applications</button>
<button class="tab-button" onclick="showTab('openid')">Unconfigured OpenID Applications</button>
</div>
<div id="saml" class="tab-content active">
<div class="summary">
Total Unconfigured SAML Applications: $($unconfiguredSaml.Count)
</div>
<ul class="app-list">
$(foreach ($app in $unconfiguredSaml) {
"<li class='app-item'>$app</li>"
})
</ul>
</div>
<div id="openid" class="tab-content">
<div class="summary">
Total Unconfigured OpenID Applications: $($unconfiguredOpenID.Count)
</div>
<ul class="app-list">
$(foreach ($app in $unconfiguredOpenID) {
"<li class='app-item'>$app</li>"
})
</ul>
</div>
</div>
<script>
function showTab(tabId) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
document.querySelector(`[onclick="showTab('${tabId}')"]`).classList.add('active');
}
</script>
</body>
</html>
"@
# Save the new report
$newHtmlContent | Out-File "unconfigured_applications_report.html" -Encoding UTF8
Write-Host "`nNew report has been generated as 'unconfigured_applications_report.html'"