Generate-EntraAdminRolesReport.ps1
<#PSScriptInfo
.VERSION 0.2 .GUID 1bb55298-eaa9-43fa-9b1c-f0ad57109664 .AUTHOR Roy Klooster .COMPANYNAME .COPYRIGHT .TAGS RKSolutions Microsoft365 MicrosoftEntraID MicrosoftGraph .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 0.1 - Initial version of the script, providing basic functionality to connect to Microsoft Graph and retrieve role assignments. 0.2 - Adding authentication option for system managed identity and access token Removed the need of the paramater "IncludePrivilegedAssignments" because this sometimes confusing for admins Added extra required scopes for the script to function correctly. Added PIM audit logs with filter options Fixed some UI issues in the HTML report. #> <# .DESCRIPTION This PowerShell script generates a comprehensive HTML report of Microsoft Entra ID administrative role assignments. It connects to Microsoft Graph API using multiple authentication methods (Interactive, ClientSecret, or Certificate) and retrieves all role assignments in the tenant, including permanent and eligible (PIM) assignments. The script categorizes assignments by principal type (users, groups, service principals), collects group membership details, and supports filtering by various criteria. #> Param( [Parameter(Mandatory = $false, ParameterSetName = "Interactive")] [Parameter(Mandatory = $false, ParameterSetName = "ClientSecret")] [Parameter(Mandatory = $false, ParameterSetName = "Certificate")] [Parameter(Mandatory = $false, ParameterSetName = "Identity")] [Parameter(Mandatory = $false, ParameterSetName = "AccessToken")] [string[]]$RequiredScopes = @("Directory.Read.All", "PrivilegedEligibilitySchedule.Read.AzureADGroup", "Organization.Read.All", "AuditLog.Read.All", "RoleManagement.Read.Directory"), [Parameter(Mandatory = $true, ParameterSetName = "ClientSecret")] [Parameter(Mandatory = $true, ParameterSetName = "Certificate")] [Parameter(Mandatory = $false, ParameterSetName = "Identity")] [Parameter(Mandatory = $true, ParameterSetName = "AccessToken")] [Parameter(Mandatory = $false, ParameterSetName = "Interactive")] [string]$TenantId, [Parameter(Mandatory = $true, ParameterSetName = "ClientSecret")] [Parameter(Mandatory = $true, ParameterSetName = "Certificate")] [Parameter(Mandatory = $false, ParameterSetName = "Interactive")] [string]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = "ClientSecret")] [string]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = "Certificate")] [string]$CertificateThumbprint, [Parameter(Mandatory = $true, ParameterSetName = "Identity")] [switch]$Identity, [Parameter(Mandatory = $true, ParameterSetName = "AccessToken")] [string]$AccessToken ) function New-AdminRoleHTMLReport { param( [Parameter(Mandatory = $true)] [string]$TenantName, [Parameter(Mandatory = $true)] [array]$Report, [Parameter(Mandatory = $false)] [array]$GroupAssignmentReport, [Parameter(Mandatory = $false)] [array]$ServicePrincipalReport, [Parameter(Mandatory = $false)] [array]$UserAssignmentReport, [Parameter(Mandatory = $false)] [array]$GroupMembershipOverviewReport, [Parameter(Mandatory = $false)] [array]$PIMAuditLogsReport, [Parameter(Mandatory = $false)] [string]$ExportPath = "$env:PUBLIC\Documents\$TenantName-AdminRolesReport.html" ) # Calculate roles counts for dashboard statistics $permanentRoles = ($Report | Where-Object { $_.AssignmentType -eq "Permanent" }).Count $eligibleRoles = ($Report | Where-Object { $_.AssignmentType -like "Eligible*" }).Count $groupAssignedRoles = $GroupAssignmentReport.Count $servicePrincipalRoles = $ServicePrincipalReport.Count # Get the current date and time for the report header $CurrentDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") # Create HTML Template with DataTables $htmlTemplate = @' <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>$TenantName Admin Roles Report</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.1/css/buttons.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <script src="https://code.jquery.com/jquery-3.7.0.js"></script> <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.bootstrap5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/pdfmake.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/vfs_fonts.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.print.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.colVis.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <style> :root { /* Light mode variables (default) */ --primary-color: #0078d4; --secondary-color: #2b88d8; --permanent-color: #d83b01; --eligible-color: #107c10; --group-color: #5c2d91; --disabled-color: #d9534f; --enabled-color: #5cb85c; --service-principal-color: #0078d4; --na-color: #6c757d; --bg-color: #f8f9fa; --card-bg: #ffffff; --text-color: #333333; --table-header-bg: #f5f5f5; --table-header-color: #333333; --table-stripe-bg: rgba(0,0,0,0.02); --table-hover-bg: rgba(0,0,0,0.04); --table-border-color: #dee2e6; --filter-tag-bg: #e9ecef; --filter-tag-color: #495057; --filter-bg: white; --btn-outline-color: #6c757d; --border-color: #dee2e6; --toggle-bg: #ccc; --button-bg: #f8f9fa; --button-color: #333; --button-border: #ddd; --button-hover-bg: #e9ecef; --footer-text: white; --input-bg: #fff; --input-color: #333; --input-border: #ced4da; --input-focus-border: #86b7fe; --input-focus-shadow: rgba(13, 110, 253, 0.25); --datatable-even-row-bg: #fff; --datatable --tab-active-color: #fff; } [data-theme="dark"] { /* Dark mode variables */ --primary-color: #0078d4; --secondary-color: #2b88d8; --permanent-color: #d83b01; --eligible-color: #107c10; --group-color: #5c2d91; --disabled-color: #6c757d; --enabled-color: #0078d4; --service-principal-color: #0078d4; --bg-color: #121212; --card-bg: #1e1e1e; --text-color: #e0e0e0; --table-header-bg: #333333; --table-header-color: #e0e0e0; --table-header-bg: #333333;#e0e0e0;5,255,0.03); --table-header-color: #e0e0e0; --table-stripe-bg: rgba(255,255,255,0.03); --table-hover-bg: rgba(255,255,255,0.05); --table-border-color: #444444; --filter-bg: #252525; --btn-outline-color: #adb5bd; --border-color: #444444; --toggle-bg: #555555; --button-bg: #2a2a2a; --button-color: #e0e0e0; --button-border: #444; --button-hover-bg: #3a3a3a; --footer-text: white; --input-bg: #2a2a2a; --input-color: #e0e0e0; --input-border: #444444; --input-focus-border: #0078d4; --input-focus-shadow: rgba(0, 120, 212, 0.25); --datatable-even-row-bg: #1e1e1e; --datatable-odd-row-bg: #252525; --tab-active-bg: #0078d4; --tab-active-color: #fff; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-color); min-height: 100vh; display: flex; flex-direction: column; transition: background-color 0.3s ease, color 0.3s ease; } .container-fluid { max-width: 1600px; padding: 20px; flex: 1; } .dashboard-header { padding: 20px 0; margin-bottom: 30px; border-bottom: 1px solid rgba(128,128,128,0.2); display: flex; align-items: center; justify-content: space-between; } .dashboard-title { display: flex; align-items: center; gap: 15px; } .dashboard-title h1 { margin: 0; font-size: 1.8rem; font-weight: 600; color: var(--primary-color); } .logo { height: 45px; width: 45px; } .report-date { font-size: 0.9rem; color: var(--text-color); opacity: 0.8; } .card { border: none; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); margin-bottom: 25px; transition: transform 0.2s, box-shadow 0.2s, background-color 0.3s ease; overflow: hidden; background-color: var(--card-bg); } .card:hover { transform: translateY(-5px); box-shadow: 0 8px 16px rgba(0,0,0,0.1); } .card-header { background-color: var(--primary-color); color: white; font-weight: 600; padding: 15px 20px; border-bottom: none; display: flex; align-items: center; justify-content: space-between; gap: 10px; } .card-header i { font-size: 1.2rem; } .card-body { padding: 20px; } .stats-card { height: 100%; text-align: center; padding: 25px 15px; border-radius: 10px; color: white; position: relative; overflow: hidden; min-height: 160px; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s; } .stats-card.active { box-shadow: 0 0 0 4px rgba(255,255,255,0.6), 0 8px 16px rgba(0,0,0,0.2); transform: scale(1.05); } .stats-card::before { content: ''; position: absolute; top: -20px; right: -20px; width: 100px; height: 100px; border-radius: 50%; background-color: rgba(255,255,255,0.1); z-index: 0; } .stats-card i { font-size: 2.5rem; margin-bottom: 15px; position: relative; z-index: 1; } .stats-card h3 { font-size: 1rem; font-weight: 500; margin-bottom: 10px; position: relative; z-index: 1; } .stats-card .number { font-size: 2.2rem; font-weight: 700; position: relative; z-index: 1; } .permanent-bg { background: linear-gradient(135deg, var(--permanent-color), #f25c05); } .eligible-bg { background: linear-gradient(135deg, var(--eligible-color), #2a9d2a); } .group-bg { background: linear-gradient(135deg, var(--group-color), #7b4db2); } .disabled-bg { background: linear-gradient(135deg, var(--disabled-color), #6c757d); } .enabled-bg { background: linear-gradient(135deg, var(--enabled-color), #0078d4); } .service-principal-bg { background: linear-gradient(135deg, var(--service-principal-color), #2b88d8); } /* DataTables Dark Mode Overrides */ table.dataTable { border-collapse: collapse !important; width: 100% !important; color: var(--text-color) !important; border-color: var(--table-border-color) !important; } .table { color: var(--text-color) !important; border-color: var(--table-border-color) !important; } .table-striped>tbody>tr:nth-of-type(odd) { background-color: var(--datatable-odd-row-bg) !important; } .table-striped>tbody>tr:nth-of-type(even) { background-color: var(--datatable-even-row-bg) !important; } .table thead th { background-color: var(--table-header-bg) !important; color: var(--table-header-color) !important; font-weight: 600; border-top: none; padding: 12px; border-color: var(--table-border-color) !important; } .table tbody td { padding: 12px; vertical-align: middle; border-color: var(--table-border-color) !important; color: var(--text-color) !important; } .table.table-bordered { border-color: var(--table-border-color) !important; } .table-bordered td, .table-bordered th { border-color: var(--table-border-color) !important; } .table-hover tbody tr:hover { background-color: var(--table-hover-bg) !important; } .badge { padding: 6px 10px; font-weight: 500; border-radius: 6px; } .badge-permanent { background-color: var(--permanent-color); color: white; } .badge-eligible { background-color: var(--eligible-color); color: white; } .badge-active { background-color: var(--service-principal-color); color: white; } .badge-group { background-color: var(--group-color); color: white; } .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate { color: var(--text-color) !important; } .dataTables_wrapper .dataTables_paginate .paginate_button { padding: 0.3em 0.8em; border-radius: 4px; margin: 0 3px; color: var(--text-color) !important; border: 1px solid var(--border-color) !important; background-color: var(--button-bg) !important; } .dataTables_wrapper .dataTables_paginate .paginate_button.current { background: var(--primary-color) !important; border-color: var(--primary-color) !important; color: white !important; } .dataTables_wrapper .dataTables_paginate .paginate_button:hover { background: var(--button-hover-bg) !important; border-color: var(--border-color) !important; color: var(--text-color) !important; } .dataTables_wrapper .dataTables_length select, .dataTables_wrapper .dataTables_filter input { border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--input-color); border-radius: 4px; padding: 5px 10px; } .dataTables_wrapper .dataTables_filter input:focus { border-color: var(--input-focus-border); box-shadow: 0 0 0 0.25rem var(--input-focus-shadow); } .dataTables_info { padding-top: 10px; color: var(--text-color); } footer { background-color: var(--primary-color); color: var(--footer-text); text-align: center; padding: 15px 0; margin-top: auto; } footer p { margin: 0; font-weight: 500; } .filter-buttons { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } .filter-button { padding: 8px 16px; border-radius: 20px; border: none; cursor: pointer; font-weight: 500; transition: all 0.2s; display: flex; align-items: center; gap: 8px; } .filter-button:hover { opacity: 0.9; } .filter-button.active { box-shadow: 0 0 0 2px rgba(128,128,128,0.2); } .toggle-container { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; } .toggle-switch { position: relative; display: inline-block; width: 60px; height: 30px; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--toggle-bg); transition: .4s; border-radius: 34px; } .toggle-slider:before { position: absolute; content: ""; height: 22px; width: 22px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .toggle-slider { background-color: var(--primary-color); } input:checked + .toggle-slider:before { transform: translateX(30px); } .filter-section { background-color: var(--filter-bg); padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 20px; transition: background-color 0.3s ease; } .filter-section h5 { color: var(--primary-color); margin-bottom: 12px; font-weight: 600; } .filter-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; } .filter-tag { background-color: var(--filter-tag-bg); padding: 4px 12px; border-radius: 16px; font-size: 0.85rem; color: var(--filter-tag-color); display: flex; align-items: center; gap: 6px; transition: background-color 0.3s ease, color 0.3s ease; } .filter-tag i { cursor: pointer; color: var(--filter-tag-color); } .filter-tag i:hover { color: var(--permanent-color); } .custom-search { display: flex; gap: 10px; margin-bottom: 15px; } .custom-search input { flex: 1; padding: 8px 12px; border: 1px solid var(--border-color); background-color: var(--bg-color); color: var(--text-color); border-radius: 4px; } .custom-search button { background-color: var(--primary-color); color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; } .enabled-filters-container { margin-bottom: 15px; } /* Show all entries toggle */ .show-all-container { display: flex; align-items: center; gap: 12px; background-color: transparent; padding: 0; border: none; margin-left: 15px; } .show-all-text { font-weight: 500; margin: 0; color: white; font-size: 0.85rem; } /* Custom DataTable controls wrapper */ .datatable-header { display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; } .datatable-controls { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; } /* Theme toggle styles */ .theme-toggle { position: fixed; top: 20px; right: 20px; z-index: 1000; display: flex; align-items: center; gap: 10px; background-color: var(--card-bg); padding: 8px 12px; border-radius: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: background-color 0.3s ease; } .theme-toggle-switch { position: relative; display: inline-block; width: 50px; height: 26px; } .theme-toggle-switch input { opacity: 0; width: 0; height: 0; } .theme-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--toggle-bg); transition: .4s; border-radius: 34px; } .theme-toggle-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .theme-toggle-slider { background-color: var(--primary-color); } input:checked + .theme-toggle-slider:before { transform: translateX(24px); } .theme-icon { display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--text-color); } /* Form elements for dark mode */ .form-select, .form-control { background-color: var(--input-bg) !important; color: var(--input-color) !important; border-color: var(--input-border) !important; } .form-select:focus, .form-control:focus { border-color: var(--input-focus-border) !important; box-shadow: 0 0 0 0.25rem var(--input-focus-shadow) !important; } .form-label { color: var(--text-color); } .btn-outline-secondary { color: var(--text-color); border-color: var(--border-color); background-color: transparent; } .btn-outline-secondary:hover { background-color: var(--filter-tag-bg); color: var(--text-color); } /* Override for dropdown menus and selects */ .form-select option { background-color: var(--input-bg); color: var(--input-color); } /* Fix DataTables odd/even row striping */ table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd { background-color: var(--datatable-odd-row-bg) !important; } table.dataTable.stripe tbody tr.even, table.dataTable.display tbody tr.even { background-color: var(--datatable-even-row-bg) !important; } /* Fix DataTables background color for hovered rows */ table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { background-color: var(--table-hover-bg) !important; } /* Fix DataTables border colors */ table.dataTable.border-bottom, table.dataTable.border-top, table.dataTable thead th, table.dataTable tfoot th, table.dataTable thead td, table.dataTable tfoot td { border-color: var(--table-border-color) !important; } /* Bootstrap 5 DataTables specific overrides */ .table-striped>tbody>tr:nth-of-type(odd)>* { --bs-table-accent-bg: var(--datatable-odd-row-bg) !important; color: var(--text-color) !important; } .table>:not(caption)>*>* { background-color: var(--card-bg) !important; color: var(--text-color) !important; } .table-striped>tbody>tr { background-color: var(--datatable-even-row-bg) !important; } /* Direct cell background colors */ .table tbody tr td { background-color: transparent !important; } /* Force Bootstrap Tables to use the correct colors */ .table-striped>tbody>tr:nth-of-type(odd) { --bs-table-accent-bg: var(--datatable-odd-row-bg) !important; } /* Report selector tabs */ .report-tabs { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } .report-tab { padding: 10px 20px; border-radius: 5px; background-color: var(--button-bg); color: var(--button-color); border: 1px solid var(--button-border); cursor: pointer; font-weight: 600; transition: all 0.2s; } .report-tab:hover { background-color: var(--button-hover-bg); } .report-tab.active { background-color: var(--tab-active-bg); color: var(--tab-active-color); border-color: var(--tab-active-bg); } .report-panel { display: none; } .report-panel.active { display: block; } </style> </head> <body> <!-- Dark Mode Toggle --> <div class="theme-toggle"> <div class="theme-icon"> <i class="fas fa-sun"></i> </div> <label class="theme-toggle-switch"> <input type="checkbox" id="themeToggle"> <span class="theme-toggle-slider"></span> </label> <div class="theme-icon"> <i class="fas fa-moon"></i> </div> </div> <div class="container-fluid"> <div class="dashboard-header"> <div class="dashboard-title"> <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"> <path fill="#ff5722" d="M6 6H22V22H6z" transform="rotate(-180 14 14)"/> <path fill="#4caf50" d="M26 6H42V22H26z" transform="rotate(-180 34 14)"/> <path fill="#ffc107" d="M6 26H22V42H6z" transform="rotate(-180 14 34)"/> <path fill="#03a9f4" d="M26 26H42V42H26z" transform="rotate(-180 34 34)"/> </svg> <h1>$TenantName Admin Roles Report</h1> </div> <div class="report-date"> <i class="fas fa-calendar-alt me-2"></i> Report generated on: $ReportDate </div> </div> <div class="row mb-4"> <div class="col-md-3 mb-3"> <div class="stats-card permanent-bg" id="permanentFilter"> <i class="fas fa-key"></i> <h3>Permanent Assignments</h3> <div class="number">$permanentRoles</div> </div> </div> <div class="col-md-3 mb-3"> <div class="stats-card eligible-bg" id="eligibleFilter"> <i class="fas fa-clock"></i> <h3>Eligible Assignments</h3> <div class="number">$eligibleRoles</div> </div> </div> <div class="col-md-3 mb-3"> <div class="stats-card group-bg" id="groupFilter"> <i class="fas fa-users"></i> <h3>Group Assignments</h3> <div class="number">$groupAssignedRoles</div> </div> </div> <div class="col-md-3 mb-3"> <div class="stats-card service-principal-bg" id="spFilter"> <i class="fas fa-robot"></i> <h3>Service Principal Assignments</h3> <div class="number">$servicePrincipalRoles</div> </div> </div> </div> <div class="report-tabs"> </div> <div class="filter-section" id="general-filter-section"> <h5><i class="fas fa-filter me-2"></i>Filter Options</h5> <div class="row"> <div class="col-md-6"> <div class="mb-3"> <label for="principalTypeFilter" class="form-label">Principal Type</label> <select id="principalTypeFilter" class="form-select"> <option value="">All Types</option> <option value="user">User</option> <option value="group">Group</option> <option value="service Principal">Service Principal</option> </select> </div> </div> <div class="col-md-6"> <div class="mb-3"> <label for="assignmentTypeFilter" class="form-label">Assignment Type</label> <select id="assignmentTypeFilter" class="form-select"> <option value="">All Types</option> <option value="Permanent">Permanent</option> <option value="Eligible">Eligible</option> </select> </div> </div> </div> <div class="mb-3"> <label for="roleNameFilter" class="form-label">Role Name</label> <input type="text" id="roleNameFilter" class="form-control" placeholder="Search for role names..."> </div> <div class="mb-3"> <label for="scopeFilter" class="form-label">Role Scope</label> <select id="scopeFilter" class="form-select"> <option value="">All Scopes</option> <option value="Tenant-Wide">Tenant-Wide</option> <option value="AU/">Administrative Unit</option> </select> </div> <div class="enabled-filters-container"> <div class="d-flex justify-content-between align-items-center"> <label class="form-label mb-0">Enabled Filters:</label> <button id="clearAllFilters" class="btn btn-sm btn-outline-secondary">Clear All</button> </div> <div class="filter-tags" id="enabledFilters"> <!-- Enabled filters will be displayed here --> </div> </div> </div> <div id="all-report" class="report-panel active"> <div class="card"> <div class="card-header"> <div> <i class="fas fa-user-shield"></i> All Role Assignments </div> <div class="show-all-container"> <label class="toggle-switch"> <input type="checkbox" id="allShowAllToggle"> <span class="toggle-slider"></span> </label> <p class="show-all-text">Show all entries</p> </div> </div> <div class="card-body"> <div class="table-responsive"> <table id="allRolesTable" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>Principal</th> <th>Display Name</th> <th>Principal Type</th> <th>Account Status</th> <th>Assigned Role</th> <th>Role Scope</th> <th>Assignment Type</th> <th>Start Date</th> <th>End Date</th> </tr> </thead> <tbody> {{ALL_ROLES_DATA}} </tbody> </table> </div> </div> </div> </div> <div id="user-report" class="report-panel"> <div class="card"> <div class="card-header"> <div> <i class="fas fa-user"></i> User Role Assignments </div> <div class="show-all-container"> <label class="toggle-switch"> <input type="checkbox" id="userShowAllToggle"> <span class="toggle-slider"></span> </label> <p class="show-all-text">Show all entries</p> </div> </div> <div class="card-body"> <div class="table-responsive"> <table id="userRolesTable" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>Principal</th> <th>Display Name</th> <th>Principal Type</th> <th>Account Status</th> <th>Assigned Role</th> <th>Role Scope</th> <th>Assignment Type</th> <th>Start Date</th> <th>End Date</th> </tr> </thead> <tbody> {{USER_ROLES_DATA}} </tbody> </table> </div> </div> </div> </div> <div id="group-report" class="report-panel"> <div class="card"> <div class="card-header"> <div> <i class="fas fa-users"></i> Group Role Assignments </div> <div class="show-all-container"> <label class="toggle-switch"> <input type="checkbox" id="groupShowAllToggle"> <span class="toggle-slider"></span> </label> <p class="show-all-text">Show all entries</p> </div> </div> <div class="card-body"> <div class="table-responsive"> <table id="groupRolesTable" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>Principal</th> <th>Display Name</th> <th>Principal Type</th> <th>Account Status</th> <th>Assigned Role</th> <th>Role Scope</th> <th>Assignment Type</th> <th>Start Date</th> <th>End Date</th> <th>Members</th> </tr> </thead> <tbody> {{GROUP_ROLES_DATA}} </tbody> </table> </div> </div> </div> </div> <div id="pim-audit-logs-report" class="report-panel"> <div class="filter-section" id="pim-filter-section"> <h5><i class="fas fa-filter me-2"></i>PIM Audit Logs Filters</h5> <div class="row"> <div class="col-md-4"> <div class="mb-3"> <label for="pimOperationTypeFilter" class="form-label">Operation Type</label> <select id="pimOperationTypeFilter" class="form-select"> <option value="">All Operations</option> <option value="Add">Add</option> <option value="Update">Update</option> <option value="Delete">Delete</option> <option value="Activate">Activate</option> </select> </div> </div> <div class="col-md-4"> <div class="mb-3"> <label for="pimInitiatorFilter" class="form-label">Initiated By</label> <input type="text" id="pimInitiatorFilter" class="form-control" placeholder="Filter by user..."> </div> </div> <div class="col-md-4"> <div class="mb-3"> <label for="pimResultFilter" class="form-label">Result</label> <select id="pimResultFilter" class="form-select"> <option value="">All Results</option> <option value="Success">Success</option> <option value="Failure">Failure</option> </select> </div> </div> </div> <div class="row"> <div class="col-md-4"> <div class="mb-3"> <label for="pimRoleFilter" class="form-label">Role</label> <input type="text" id="pimRoleFilter" class="form-control" placeholder="Filter by role..."> </div> </div> <div class="col-md-4"> <div class="mb-3"> <label for="pimTargetFilter" class="form-label">Target User</label> <input type="text" id="pimTargetFilter" class="form-control" placeholder="Filter by target user..."> </div> </div> <div class="col-md-4"> <div class="mb-3"> <label for="pimDateRangeFilter" class="form-label">Date Range</label> <div class="input-group"> <input type="date" id="pimStartDateFilter" class="form-control"> <span class="input-group-text">to</span> <input type="date" id="pimEndDateFilter" class="form-control"> </div> </div> </div> </div> <div class="enabled-filters-container"> <div class="d-flex justify-content-between align-items-center"> <label class="form-label mb-0">PIM Audit Logs Filters:</label> <button id="clearPimFilters" class="btn btn-sm btn-outline-secondary">Clear Filters</button> </div> <div class="filter-tags" id="pimEnabledFilters"> <!-- Enabled PIM filters will be displayed here --> </div> </div> </div> <div class="card"> <div class="card-header"> <div> <i class="fas fa-history"></i> PIM Audit Logs </div> <div class="show-all-container"> <label class="toggle-switch"> <input type="checkbox" id="pimAuditLogsShowAllToggle"> <span class="toggle-slider"></span> </label> <p class="show-all-text">Show all entries</p> </div> </div> <div class="card-body"> <div class="table-responsive"> <table id="pimAuditLogsTable" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>Date/Time</th> <th>Initiated By</th> <th>Operation Type</th> <th>Initiator Type</th> <th>Role</th> <th>Target</th> <th>Operation</th> <th>Result</th> <th>Role Properties</th> <th>Target</th> <th>Operation</th> <th>Result</th> <th>Justification</th> </tr> </thead> <tbody> {{PIM_AUDIT_LOGS_DATA}} </tbody> </table> </div> </div> </div> </div> <footer> <p>Generated by Roy Klooster - RK Solutions</p> </footer> <script> // Initialize DataTables $(document).ready(function() { // Theme toggling functionality const themeToggle = document.getElementById('themeToggle'); const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); // Function to update table colors for dark mode function updateTableColors() { // Force all table cells to have the correct background if (document.documentElement.getAttribute('data-theme') === 'dark') { // Dark mode $('table.dataTable tbody tr').css('background-color', 'var(--datatable-even-row-bg)'); $('table.dataTable tbody tr:nth-child(odd)').css('background-color', 'var(--datatable-odd-row-bg)'); $('table.dataTable tbody td').css('color', 'var(--text-color)'); $('table.dataTable thead th').css({ 'background-color': 'var(--table-header-bg)', 'color': 'var(--table-header-color)' }); } else { // Light mode $('table.dataTable tbody tr').css('background-color', 'var(--datatable-even-row-bg)'); $('table.dataTable tbody tr:nth-child(odd)').css('background-color', 'var(--datatable-odd-row-bg)'); $('table.dataTable tbody td').css('color', 'var(--text-color)'); $('table.dataTable thead th').css({ 'background-color': 'var(--table-header-bg)', 'color': 'var(--table-header-color)' }); } } // Check for saved user preference, or use system preference const savedTheme = localStorage.getItem('theme'); if (savedTheme === 'dark' || (!savedTheme && prefersDarkScheme.matches)) { document.documentElement.setAttribute('data-theme', 'dark'); themeToggle.checked = true; } // Add event listener for theme toggle themeToggle.addEventListener('change', function() { if (this.checked) { document.documentElement.setAttribute('data-theme', 'dark'); localStorage.setItem('theme', 'dark'); } else { document.documentElement.setAttribute('data-theme', 'light'); localStorage.setItem('theme', 'light'); } // Apply the table color changes after theme switch setTimeout(updateTableColors, 50); }); // Tab switching functionality with filter section toggle $('.report-tab').on('click', function() { // Remove active class from all tabs and panels $('.report-tab').removeClass('active'); $('.report-panel').removeClass('active'); // Add active class to clicked tab and corresponding panel $(this).addClass('active'); const panelId = $(this).data('panel'); $(`#${panelId}`).addClass('active'); // Toggle visibility of filter sections based on active tab // Initialize filter visibility - hide PIM filter by default $('#pim-filter-section').hide(); // Adjust DataTables columns when switching tabs setTimeout(function() { $.fn.dataTable.tables({ visible: true, api: true }).columns.adjust(); }, 10); }); // Initialize DataTable for all roles const allRolesTable = $('#allRolesTable').DataTable({ dom: 'Bfrtip', buttons: [ { extend: 'collection', text: '<i class="fas fa-download"></i> Export', buttons: [ { extend: 'excel', text: '<i class="fas fa-file-excel"></i> Excel', exportOptions: { columns: ':visible' } }, { extend: 'csv', text: '<i class="fas fa-file-csv"></i> CSV', exportOptions: { columns: ':visible' } }, { extend: 'pdf', text: '<i class="fas fa-file-pdf"></i> PDF', exportOptions: { columns: ':visible' } }, { extend: 'print', text: '<i class="fas fa-print"></i> Print', exportOptions: { columns: ':visible' } } ] }, { extend: 'colvis', text: '<i class="fas fa-columns"></i> Columns' } ], paging: true, searching: true, ordering: true, info: true, responsive: true, lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], order: [[3, 'asc']], language: { search: "<i class='fas fa-search'></i> _INPUT_", searchPlaceholder: "Search records...", lengthMenu: "Show _MENU_ entries", info: "Showing _START_ to _END_ of _TOTAL_ entries", paginate: { first: "<i class='fas fa-angle-double-left'></i>", last: "<i class='fas fa-angle-double-right'></i>", next: "<i class='fas fa-angle-right'></i>", previous: "<i class='fas fa-angle-left'></i>" } }, drawCallback: function() { updateTableColors(); } }); // Initialize DataTable for user roles const userRolesTable = $('#userRolesTable').DataTable({ dom: 'Bfrtip', buttons: [ { extend: 'collection', text: '<i class="fas fa-download"></i> Export', buttons: [ { extend: 'excel', text: '<i class="fas fa-file-excel"></i> Excel', exportOptions: { columns: ':visible' } }, { extend: 'csv', text: '<i class="fas fa-file-csv"></i> CSV', exportOptions: { columns: ':visible' } }, { extend: 'pdf', text: '<i class="fas fa-file-pdf"></i> PDF', exportOptions: { columns: ':visible' } }, { extend: 'print', text: '<i class="fas fa-print"></i> Print', exportOptions: { columns: ':visible' } } ] }, { extend: 'colvis', text: '<i class="fas fa-columns"></i> Columns' } ], paging: true, searching: true, ordering: true, info: true, responsive: true, lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], order: [[3, 'asc']], language: { search: "<i class='fas fa-search'></i> _INPUT_", searchPlaceholder: "Search records..." }, drawCallback: function() { updateTableColors(); } }); // Initialize DataTable for group roles const groupRolesTable = $('#groupRolesTable').DataTable({ dom: 'Bfrtip', buttons: [ { extend: 'collection', text: '<i class="fas fa-download"></i> Export', buttons: [ { extend: 'excel', text: '<i class="fas fa-file-excel"></i> Excel', exportOptions: { columns: ':visible' } }, { extend: 'csv', text: '<i class="fas fa-file-csv"></i> CSV', exportOptions: { columns: ':visible' } }, { extend: 'pdf', text: '<i class="fas fa-file-pdf"></i> PDF', exportOptions: { columns: ':visible' } }, { extend: 'print', text: '<i class="fas fa-print"></i> Print', exportOptions: { columns: ':visible' } } ] }, { extend: 'colvis', text: '<i class="fas fa-columns"></i> Columns' } ], paging: true, searching: true, ordering: true, info: true, responsive: true, lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], order: [[3, 'asc']], language: { search: "<i class='fas fa-search'></i> _INPUT_", searchPlaceholder: "Search records..." }, drawCallback: function() { updateTableColors(); } }); // Initialize DataTable for service principal roles const spRolesTable = $('#spRolesTable').DataTable({ dom: 'Bfrtip', buttons: [ { extend: 'collection', text: '<i class="fas fa-download"></i> Export', buttons: [ { extend: 'excel', text: '<i class="fas fa-file-excel"></i> Excel', exportOptions: { columns: ':visible' } }, { extend: 'csv', text: '<i class="fas fa-file-csv"></i> CSV', exportOptions: { columns: ':visible' } }, { extend: 'pdf', text: '<i class="fas fa-file-pdf"></i> PDF', exportOptions: { columns: ':visible' } }, { extend: 'print', text: '<i class="fas fa-print"></i> Print', exportOptions: { columns: ':visible' } } ] }, { extend: 'colvis', text: '<i class="fas fa-columns"></i> Columns' } ], paging: true, searching: true, ordering: true, info: true, responsive: true, lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], order: [[3, 'asc']], language: { search: "<i class='fas fa-search'></i> _INPUT_", searchPlaceholder: "Search records..." }, drawCallback: function() { updateTableColors(); } }); // Initialize DataTable for PIM audit logs const pimAuditLogsTable = $('#pimAuditLogsTable').DataTable({ dom: 'Bfrtip', buttons: [ { extend: 'collection', text: '<i class="fas fa-download"></i> Export', buttons: [ { extend: 'excel', text: '<i class="fas fa-file-excel"></i> Excel', exportOptions: { columns: ':visible' } }, { extend: 'csv', text: '<i class="fas fa-file-csv"></i> CSV', exportOptions: { columns: ':visible' } }, { extend: 'pdf', text: '<i class="fas fa-file-pdf"></i> PDF', exportOptions: { columns: ':visible' } }, { extend: 'print', text: '<i class="fas fa-print"></i> Print', exportOptions: { columns: ':visible' } } ] }, { extend: 'colvis', text: '<i class="fas fa-columns"></i> Columns' } ], paging: true, searching: true, ordering: true, info: true, scrollX: true, scrollCollapse: true, fixedHeader: true, responsive: true, lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], order: [[0, 'desc']], // Sort by date/time descending language: { search: "<i class='fas fa-search'></i> _INPUT_", searchPlaceholder: "Search records..." }, drawCallback: function() { updateTableColors(); // Ensure the search and pagination controls maintain their position $('.dataTables_filter, .dataTables_paginate').css({ 'position': 'sticky', 'right': '0', 'background-color': 'var(--card-bg)', 'z-index': '10' }); } }); // Apply initial table colors setTimeout(updateTableColors, 100); // Show all toggle functionality $('#pimAuditLogsShowAllToggle').on('change', function() { if ($(this).is(':checked')) { pimAuditLogsTable.page.len(-1).draw(); } else { pimAuditLogsTable.page.len(10).draw(); } }); // Add these event handlers for the other toggles: $('#allShowAllToggle').on('change', function() { if ($(this).is(':checked')) { allRolesTable.page.len(-1).draw(); } else { allRolesTable.page.len(10).draw(); } }); $('#userShowAllToggle').on('change', function() { if ($(this).is(':checked')) { userRolesTable.page.len(-1).draw(); } else { userRolesTable.page.len(10).draw(); } }); $('#groupShowAllToggle').on('change', function() { if ($(this).is(':checked')) { groupRolesTable.page.len(-1).draw(); } else { groupRolesTable.page.len(10).draw(); } }); // Add this handler if you have a service principal toggle as well $('#spShowAllToggle').on('change', function() { if ($(this).is(':checked')) { spRolesTable.page.len(-1).draw(); } else { spRolesTable.page.len(10).draw(); } }); // Custom filtering function for PIM audit logs $.fn.dataTable.ext.search.push( function(settings, data, dataIndex) { // Only apply to PIM audit logs table if (settings.nTable.id !== 'pimAuditLogsTable') { return true; } // Get PIM filter values const operationType = $('#pimOperationTypeFilter').val().toLowerCase(); const initiator = $('#pimInitiatorFilter').val().toLowerCase(); const result = $('#pimResultFilter').val().toLowerCase(); const role = $('#pimRoleFilter').val().toLowerCase(); const target = $('#pimTargetFilter').val().toLowerCase(); const startDate = $('#pimStartDateFilter').val(); const endDate = $('#pimEndDateFilter').val(); // Get row data with column indices for PIM audit logs table const colDateTime = data[0]; // Date/Time column const colInitiator = data[1].toLowerCase(); // Initiated By column const colOperation = data[2].toLowerCase(); // Operation Type column const colRole = data[4].toLowerCase(); // Role column const colTarget = data[5].toLowerCase(); // Target column const colResult = data[7].toLowerCase(); // Result column // Parse date from the colDateTime value let rowDate = null; try { // Extract date part from "MM/DD/YYYY HH:MM:SS" format const dateMatch = colDateTime.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (dateMatch) { // Create date object (month is 0-indexed in JavaScript) rowDate = new Date(dateMatch[3], dateMatch[1] - 1, dateMatch[2]); } else { // Try parsing as ISO date if not in the expected format rowDate = new Date(colDateTime); } } catch (e) { // If date parsing fails, skip date filtering for this row console.warn("Failed to parse date:", colDateTime); } // Filter by operation type if (operationType && !colOperation.includes(operationType)) { return false; } // Filter by initiator if (initiator && !colInitiator.includes(initiator)) { return false; } // Filter by result if (result && !colResult.includes(result)) { return false; } // Filter by role if (role && !colRole.includes(role)) { return false; } // Filter by target if (target && !colTarget.includes(target)) { return false; } // Filter by date range if (startDate && endDate && rowDate) { const filterStartDate = new Date(startDate); const filterEndDate = new Date(endDate); // Set end date to end of day for inclusive filtering filterEndDate.setHours(23, 59, 59, 999); if (rowDate < filterStartDate || rowDate > filterEndDate) { return false; } } else if (startDate && rowDate) { const filterStartDate = new Date(startDate); if (rowDate < filterStartDate) { return false; } } else if (endDate && rowDate) { const filterEndDate = new Date(endDate); filterEndDate.setHours(23, 59, 59, 999); if (rowDate > filterEndDate) { return false; } } return true; } ); // Apply PIM filters when select inputs change $('#pimOperationTypeFilter, #pimResultFilter').on('change', function() { const filterType = $(this).attr('id'); const filterValue = $(this).val(); const filterLabel = $(this).prev('label').text(); if (filterValue) { updatePimEnabledFilter(filterLabel, filterValue); } else { removePimEnabledFilter(filterLabel); } applyPimFilters(); }); // Apply PIM filters on text input $('#pimInitiatorFilter, #pimRoleFilter, #pimTargetFilter').on('input', function() { const filterType = $(this).attr('id'); const filterValue = $(this).val(); const filterLabel = $(this).prev('label').text(); if (filterValue) { updatePimEnabledFilter(filterLabel, filterValue); } else { removePimEnabledFilter(filterLabel); } // Slight delay for typing clearTimeout($(this).data('timeout')); $(this).data('timeout', setTimeout(function() { applyPimFilters(); }, 300)); }); // Apply date range filter $('#pimStartDateFilter, #pimEndDateFilter').on('change', function() { const startDate = $('#pimStartDateFilter').val(); const endDate = $('#pimEndDateFilter').val(); if (startDate || endDate) { let dateRangeText = ''; if (startDate && endDate) { dateRangeText = `${startDate} to ${endDate}`; } else if (startDate) { dateRangeText = `From ${startDate}`; } else if (endDate) { dateRangeText = `Until ${endDate}`; } updatePimEnabledFilter('Date Range', dateRangeText); } else { removePimEnabledFilter('Date Range'); } applyPimFilters(); }); // Clear PIM filters button $('#clearPimFilters').on('click', function() { // Reset all PIM filter inputs $('#pimOperationTypeFilter').val(''); $('#pimInitiatorFilter').val(''); $('#pimResultFilter').val(''); $('#pimRoleFilter').val(''); $('#pimTargetFilter').val(''); $('#pimStartDateFilter').val(''); $('#pimEndDateFilter').val(''); // Clear the PIM filter tags $('#pimEnabledFilters').empty(); // Apply the filters (with all values cleared) applyPimFilters(); }); // Function to apply PIM filters function applyPimFilters() { pimAuditLogsTable.draw(); } // Function to update PIM enabled filters function updatePimEnabledFilter(filterType, filterValue) { // Remove existing filter of the same type removePimEnabledFilter(filterType); // Add new filter tag const filterTag = ` <div class="filter-tag" data-pim-filter-type="${filterType}"> <span>${filterType}: ${filterValue}</span> <i class="fas fa-times-circle remove-pim-filter" data-pim-filter-type="${filterType}"></i> </div> `; $('#pimEnabledFilters').append(filterTag); // Add click handler to remove filter $('.remove-pim-filter').off('click').on('click', function() { const filterTypeToRemove = $(this).data('pim-filter-type'); if (filterTypeToRemove === 'Operation Type') { $('#pimOperationTypeFilter').val(''); } else if (filterTypeToRemove === 'Initiated By') { $('#pimInitiatorFilter').val(''); } else if (filterTypeToRemove === 'Result') { $('#pimResultFilter').val(''); } else if (filterTypeToRemove === 'Role') { $('#pimRoleFilter').val(''); } else if (filterTypeToRemove === 'Target User') { $('#pimTargetFilter').val(''); } else if (filterTypeToRemove === 'Date Range') { $('#pimStartDateFilter').val(''); $('#pimEndDateFilter').val(''); } $(this).closest('.filter-tag').remove(); applyPimFilters(); }); } // Function to remove PIM enabled filter by type function removePimEnabledFilter(filterType) { $('.filter-tag[data-pim-filter-type="' + filterType + '"]').remove(); } // Make PIM filter options visible when tab is active $('.report-tab').on('click', function() { // Remove active class from all tabs and panels $('.report-tab').removeClass('active'); $('.report-panel').removeClass('active'); // Add active class to clicked tab and corresponding panel $(this).addClass('active'); const panelId = $(this).data('panel'); $(`#${panelId}`).addClass('active'); // Toggle visibility of filter sections based on active tab if (panelId === 'pim-audit-logs-report') { // When PIM audit logs tab is active $('#general-filter-section').hide(); // Hide general filter section $('#pim-filter-section').show(); // Show PIM filter section // Refresh the table when switching to PIM tab to ensure correct column widths setTimeout(function() { pimAuditLogsTable.columns.adjust(); }, 10); } else { // For all other tabs $('#general-filter-section').show(); // Show general filter section $('#pim-filter-section').hide(); // Hide PIM filter section } }); // Custom filtering function for all tables $.fn.dataTable.ext.search.push( function(settings, data, dataIndex) { // Get filter values const principalType = $('#principalTypeFilter').val().toLowerCase(); const assignmentType = $('#assignmentTypeFilter').val(); const roleName = $('#roleNameFilter').val().toLowerCase(); const scopeFilter = $('#scopeFilter').val(); // Get row data with CORRECTED column indices const colPrincipalType = data[2].toLowerCase(); // Principal Type column (index 2) const colRole = data[4].toLowerCase(); // Assigned Role column (index 4) const colAssignmentType = data[6]; // Assignment Type column (index 6) const colScope = data[5]; // Scope column (index 5) // Filter by principal type if (principalType && !colPrincipalType.includes(principalType)) { return false; } // Filter by assignment type if (assignmentType && !colAssignmentType.includes(assignmentType)) { return false; } // Filter by role name if (roleName && !colRole.includes(roleName)) { return false; } // Filter by scope if (scopeFilter && !colScope.toLowerCase().includes(scopeFilter.toLowerCase())) { return false; } return true; } ); // Stats card filtering $('#permanentFilter').on('click', function() { $('#assignmentTypeFilter').val('Permanent'); updateEnabledFilters('Assignment Type', 'Permanent'); applyFilters(); toggleStatsCardEnabled('permanentFilter'); }); $('#eligibleFilter').on('click', function() { $('#assignmentTypeFilter').val('Eligible'); updateEnabledFilters('Assignment Type', 'Eligible'); applyFilters(); toggleStatsCardEnabled('eligibleFilter'); }); $('#groupFilter').on('click', function() { $('#principalTypeFilter').val('group'); updateEnabledFilters('Principal Type', 'Group'); applyFilters(); toggleStatsCardEnabled('groupFilter'); // Switch to group report tab $('.report-tab[data-panel="group-report"]').click(); }); $('#spFilter').on('click', function() { $('#principalTypeFilter').val('service Principal'); updateEnabledFilters('Principal Type', 'Service Principal'); applyFilters(); toggleStatsCardEnabled('spFilter'); // Switch to service principal report tab $('.report-tab[data-panel="service-principal-report"]').click(); }); // Apply filters when select boxes change $('#principalTypeFilter, #assignmentTypeFilter, #scopeFilter').on('change', function() { const filterType = $(this).attr('id'); const filterValue = $(this).val(); if (filterValue) { if (filterType === 'principalTypeFilter') { updateEnabledFilters('Principal Type', filterValue); } else if (filterType === 'assignmentTypeFilter') { updateEnabledFilters('Assignment Type', filterValue); } else if (filterType === 'scopeFilter') { updateEnabledFilters('Scope', filterValue); } } else { if (filterType === 'principalTypeFilter') { removeEnabledFilter('Principal Type'); } else if (filterType === 'assignmentTypeFilter') { removeEnabledFilter('Assignment Type'); } else if (filterType === 'scopeFilter') { removeEnabledFilter('Scope'); } } applyFilters(); }); // Apply filter when role name input changes $('#roleNameFilter').on('input', function() { const filterValue = $(this).val(); if (filterValue) { updateEnabledFilters('Role Name', filterValue); } else { removeEnabledFilter('Role Name'); } applyFilters(); }); // Clear all filters button $('#clearAllFilters').on('click', function() { // Reset all filter inputs $('#principalTypeFilter').val(''); $('#assignmentTypeFilter').val(''); $('#roleNameFilter').val(''); $('#scopeFilter').val(''); // Remove active state from stats cards $('.stats-card').removeClass('active'); // Clear the visible filter tags clearEnabledFilters(); // This is critical - force DataTables to redraw with cleared filters allRolesTable.search('').columns().search('').draw(); userRolesTable.search('').columns().search('').draw(); groupRolesTable.search('').columns().search('').draw(); spRolesTable.search('').columns().search('').draw(); // Apply the filters (with all values cleared) applyFilters(); // IMPORTANT: Switch back to "All Assignments" tab $('.report-tab[data-panel="all-report"]').click(); }); // Function to apply all filters function applyFilters() { allRolesTable.draw(); userRolesTable.draw(); groupRolesTable.draw(); spRolesTable.draw(); } // Function to toggle stats card active state function toggleStatsCardEnabled(cardId) { $('.stats-card').removeClass('active'); $('#' + cardId).addClass('active'); } // Function to update enabled filters function updateEnabledFilters(filterType, filterValue) { // Remove existing filter of the same type removeEnabledFilter(filterType); // Add new filter tag const filterTag = ` <div class="filter-tag" data-filter-type="${filterType}"> <span>${filterType}: ${filterValue}</span> <i class="fas fa-times-circle remove-filter" data-filter-type="${filterType}"></i> </div> `; $('#enabledFilters').append(filterTag); // Add click handler to remove filter $('.remove-filter').off('click').on('click', function() { const filterTypeToRemove = $(this).data('filter-type'); if (filterTypeToRemove === 'Principal Type') { $('#principalTypeFilter').val(''); } else if (filterTypeToRemove === 'Assignment Type') { $('#assignmentTypeFilter').val(''); } else if (filterTypeToRemove === 'Role Name') { $('#roleNameFilter').val(''); } else if (filterTypeToRemove === 'Scope') { $('#scopeFilter').val(''); } $(this).closest('.filter-tag').remove(); // Remove active state from stat cards if (filterTypeToRemove === 'Assignment Type') { if ($('#permanentFilter').hasClass('active') || $('#eligibleFilter').hasClass('active')) { $('#permanentFilter, #eligibleFilter').removeClass('active'); } } else if (filterTypeToRemove === 'Principal Type') { if ($('#groupFilter').hasClass('active') || $('#spFilter').hasClass('active')) { $('#groupFilter, #spFilter').removeClass('active'); } } applyFilters(); }); } // Function to remove enabled filter by type function removeEnabledFilter(filterType) { $('.filter-tag[data-filter-type="' + filterType + '"]').remove(); } // Function to clear all enabled filters function clearEnabledFilters() { $('#enabledFilters').empty(); } // Force dark mode to take effect on page elements $(window).on('load', function() { setTimeout(updateTableColors, 200); }); // Re-apply styles after DataTables operations allRolesTable.on('draw.dt', function() { setTimeout(updateTableColors, 50); }); userRolesTable.on('draw.dt', function() { setTimeout(updateTableColors, 50); }); groupRolesTable.on('draw.dt', function() { setTimeout(updateTableColors, 50); }); spRolesTable.on('draw.dt', function() { setTimeout(updateTableColors, 50); }); }); </script> </body> </html> '@ # Generate table rows for all role assignments $allRolesRows = "" foreach ($item in $Report) { $assignmentTypeBadge = switch ($item.AssignmentType) { "Permanent" { '<span class="badge badge-permanent">Permanent</span>' } "Eligible" { '<span class="badge badge-eligible">Eligible</span>' } default { '<span class="badge bg-secondary">Unknown</span>' } } $allRolesRows += @" <tr> <td>$($item.Principal)</td> <td>$($item.DisplayName)</td> <td>$($item.PrincipalType)</td> <td>$($item.AccountStatus)</td> <td>$($item.'Assigned Role')</td> <td>$($item.AssignedRoleScopeName)</td> <td>$assignmentTypeBadge</td> <td>$($item.AssignmentStartDate)</td> <td>$($item.AssignmentEndDate)</td> </tr> "@ } # Generate table rows for user role assignments $userRolesRows = "" foreach ($item in $UserAssignmentReport) { $assignmentTypeBadge = switch ($item.AssignmentType) { "Permanent" { '<span class="badge badge-permanent">Permanent</span>' } "Eligible" { '<span class="badge badge-eligible">Eligible</span>' } default { '<span class="badge bg-secondary">Unknown</span>' } } $userRolesRows += @" <tr> <td>$($item.Principal)</td> <td>$($item.DisplayName)</td> <td>$($item.PrincipalType)</td> <td>$($item.AccountStatus)</td> <td>$($item.'Assigned Role')</td> <td>$($item.AssignedRoleScopeName)</td> <td>$assignmentTypeBadge</td> <td>$($item.AssignmentStartDate)</td> <td>$($item.AssignmentEndDate)</td> </tr> "@ } # Generate table rows for group role assignments $groupRolesRows = "" foreach ($item in $GroupAssignmentReport) { $assignmentTypeBadge = switch ($item.AssignmentType) { "Permanent" { '<span class="badge badge-permanent">Permanent</span>' } "Eligible" { '<span class="badge badge-eligible">Eligible</span>' } default { '<span class="badge bg-secondary">Unknown</span>' } } # Get group members from the overview report $groupMembers = ($GroupMembershipOverviewReport | Where-Object { $_.Principal -eq $item.Principal }).Members if (-not $groupMembers) { $groupMembers = "None" } $groupRolesRows += @" <tr> <td>$($item.Principal)</td> <td>$($item.DisplayName)</td> <td>$($item.PrincipalType)</td> <td>$($item.AccountStatus)</td> <td>$($item.'Assigned Role')</td> <td>$($item.AssignedRoleScopeName)</td> <td>$assignmentTypeBadge</td> <td>$($item.AssignmentStartDate)</td> <td>$($item.AssignmentEndDate)</td> <td>$groupMembers</td> </tr> "@ } # Generate table rows for service principal role assignments $spRolesRows = "" foreach ($item in $ServicePrincipalReport) { $assignmentTypeBadge = switch ($item.AssignmentType) { "Permanent" { '<span class="badge badge-permanent">Permanent</span>' } "Eligible" { '<span class="badge badge-eligible">Eligible</span>' } default { '<span class="badge bg-secondary">Unknown</span>' } } $spRolesRows += @" <tr> <td>$($item.Principal)</td> <td>$($item.DisplayName)</td> <td>$($item.PrincipalType)</td> <td>$($item.AccountStatus)</td> <td>$($item.'Assigned Role')</td> <td>$($item.AssignedRoleScopeName)</td> <td>$assignmentTypeBadge</td> <td>$($item.AssignmentStartDate)</td> <td>$($item.AssignmentEndDate)</td> </tr> "@ } # Generate table rows for PIM audit logs $pimAuditLogsRows = "" if ($PIMAuditLogsReport -and $PIMAuditLogsReport.Count -gt 0) { foreach ($log in $PIMAuditLogsReport) { $resultBadge = switch ($log.Result) { "Success" { '<span class="badge badge-eligible">Success</span>' } "Failure" { '<span class="badge badge-permanent">Failure</span>' } default { '<span class="badge bg-secondary">Unknown</span>' } } $pimAuditLogsRows += @" <tr> <td>$($log.DateTime)</td> <td>$($log.InitiatedBy)</td> <td>$($log.OperationType)</td> <td>$($log.InitiatedByType)</td> <td>$($log.Role)</td> <td>$($log.Target)</td> <td>$($log.Operation)</td> <td>$resultBadge</td> <td>$($log.RoleProperties)</td> <td>$($log.Target)</td> <td>$($log.Operation)</td> <td>$($log.Result)</td> <td>$($log.Justification)</td> </tr> "@ } } # Replace placeholders in template with actual values $htmlContent = $htmlTemplate $htmlContent = $htmlContent.Replace('$TenantName', $TenantName) $htmlContent = $htmlContent.Replace('$ReportDate', $currentDate) $htmlContent = $htmlContent.Replace('$permanentRoles', $permanentRoles) $htmlContent = $htmlContent.Replace('$eligibleRoles', $eligibleRoles) $htmlContent = $htmlContent.Replace('$groupAssignedRoles', $groupAssignedRoles) $htmlContent = $htmlContent.Replace('$servicePrincipalRoles', $servicePrincipalRoles) $htmlContent = $htmlContent.Replace('{{PIM_AUDIT_LOGS_DATA}}', $pimAuditLogsRows) # Add report tabs based on available data $reportTabs = @" <div class="report-tab active" data-panel="all-report">Assignments</div> "@ if ($UserAssignmentReport.Count -gt 0) { $reportTabs += @" <div class="report-tab" data-panel="user-report" style="display:none;">User Assignments</div> "@ } if ($GroupAssignmentReport.Count -gt 0) { $reportTabs += @" <div class="report-tab" data-panel="group-report" style="display:none;">Group Assignments</div> "@ } if ($ServicePrincipalReport.Count -gt 0) { $reportTabs += @" <div class="report-tab" data-panel="service-principal-report" style="display:none;">Service Principal Assignments</div> "@ } if ($PIMAuditLogsReport -and $PIMAuditLogsReport.Count -gt 0) { $reportTabs += @" <div class="report-tab" data-panel="pim-audit-logs-report">PIM Audit Logs</div> "@ } $htmlContent = $htmlContent.Replace('<div class="report-tabs">', "<div class=`"report-tabs`">`n$reportTabs") # Replace table data placeholders $htmlContent = $htmlContent.Replace('{{ALL_ROLES_DATA}}', $allRolesRows) $htmlContent = $htmlContent.Replace('{{USER_ROLES_DATA}}', $userRolesRows) $htmlContent = $htmlContent.Replace('{{GROUP_ROLES_DATA}}', $groupRolesRows) $htmlContent = $htmlContent.Replace('{{SP_ROLES_DATA}}', $spRolesRows) # Add additional CSS for dark mode pagination $darkModePaginationCss = @" <style> [data-theme="dark"] .page-item.active .page-link { color: white !important; border-color: var(--primary-color) !important; } [data-theme="dark"] .page-item.disabled .page-link { background-color: var(--card-bg) !important; color: #6c757d !important; border-color: var(--border-color) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button { background-color: var(--button-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button.current, [data-theme="dark"] .dataTables_paginate .paginate_button.current:hover { background: var(--primary-color) !important; color: white !important; border-color: var(--primary-color) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button:hover { background: var(--button-hover-bg) !important; color: var(--text-color) !important; border-color: var(--button-border) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button.disabled, [data-theme="dark"] .dataTables_paginate .paginate_button.disabled:hover { background-color: var(--card-bg) !important; color: #6c757d !important; border-color: var(--border-color) !important; opacity: 0.6; } /* Fix for search and pagination controls when scrolling horizontally */ .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_paginate { position: sticky; right: 0; background-color: var(--card-bg); padding: 5px; z-index: 10; } .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_length { position: sticky; left: 0; background-color: var(--card-bg); padding: 5px; z-index: 10; } </style> "@ # Insert the dark mode pagination CSS before the </head> tag $htmlContent = $htmlContent.Replace('</head>', "$darkModePaginationCss`n</head>") # Export to HTML file $htmlContent | Out-File -FilePath $ExportPath -Encoding utf8 Write-Host "INFO: All actions completed successfully." Write-Host "INFO: Admin Roles Report saved to: $ExportPath" -ForegroundColor Cyan # Open the HTML file Invoke-Item $ExportPath } function Invoke-GraphRequestWithPaging { param ( [string]$Uri, [string]$Method = "GET" ) $results = @() $currentUri = $Uri do { try { $response = Invoke-MgGraphRequest -Uri $currentUri -Method $Method -OutputType PSObject -ErrorAction Stop # Add the current page of results if ($response.value) { $results += $response.value } # Get the next page URL if it exists $currentUri = $response.'@odata.nextLink' } catch { # Instead of Write-Error, set a flag or return null to indicate failure # We'll handle this in the calling code return $null } } while ($currentUri) return $results } Function Install-Requirements { $requiredModules = @( "Microsoft.Graph.Authentication" ) foreach ($module in $requiredModules) { $moduleVersion = "2.25.0" $moduleInstalled = Get-Module -ListAvailable -Name $module | Where-Object { $_.Version -eq $moduleVersion } if (-not $moduleInstalled) { Write-Host "INFO: Installing module: $module version $moduleVersion" Install-Module -Name $module -Scope CurrentUser -Force -AllowClobber -RequiredVersion $moduleVersion -SkipPublisherCheck } else { Write-Host "INFO: Module $module version $moduleVersion is already installed." } # Import the required modules foreach ($module in $requiredModules) { $moduleVersion = "2.25.0" $importedModule = Get-Module -Name $module | Where-Object { $_.Version -eq $moduleVersion } if (-not $importedModule) { try { Import-Module -Name $module -RequiredVersion $moduleVersion -Force -ErrorAction Stop Write-Host "INFO: Module $module version $moduleVersion imported successfully." } catch { Write-Host "ERROR: Failed to import module $module version $moduleVersion. Error: $_" -ForegroundColor Red throw } } else { Write-Host "INFO: Module $module version $moduleVersion was already imported." } } } } function Connect-ToMgGraph { [CmdletBinding(DefaultParameterSetName = "Interactive")] param ( [Parameter(Mandatory = $false, ParameterSetName = "Interactive")] [Parameter(Mandatory = $false, ParameterSetName = "ClientSecret")] [Parameter(Mandatory = $false, ParameterSetName = "Certificate")] [Parameter(Mandatory = $false, ParameterSetName = "Identity")] [Parameter(Mandatory = $false, ParameterSetName = "AccessToken")] [string[]]$RequiredScopes = @("User.Read"), [Parameter(Mandatory = $true, ParameterSetName = "ClientSecret")] [Parameter(Mandatory = $true, ParameterSetName = "Certificate")] [Parameter(Mandatory = $false, ParameterSetName = "Identity")] [Parameter(Mandatory = $true, ParameterSetName = "AccessToken")] [string]$TenantId, [Parameter(Mandatory = $true, ParameterSetName = "ClientSecret")] [Parameter(Mandatory = $true, ParameterSetName = "Certificate")] [string]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = "ClientSecret")] [string]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = "Certificate")] [string]$CertificateThumbprint, [Parameter(Mandatory = $true, ParameterSetName = "Identity")] [switch]$Identity, [Parameter(Mandatory = $true, ParameterSetName = "AccessToken")] [string]$AccessToken ) # Check if Microsoft Graph module is installed and import it Install-Requirements # Determine authentication method based on parameters $AuthMethod = $PSCmdlet.ParameterSetName Write-Verbose "Using authentication method: $AuthMethod" # Check if already connected $contextInfo = Get-MgContext -ErrorAction SilentlyContinue $reconnect = $false if ($contextInfo) { # Check if we have all the required permissions for interactive auth if ($AuthMethod -eq "Interactive") { $currentScopes = $contextInfo.Scopes $missingScopes = $RequiredScopes | Where-Object { $_ -notin $currentScopes } if ($missingScopes) { Write-Verbose "Missing required scopes: $($missingScopes -join ', ')" Write-Verbose "Reconnecting with all required scopes..." $reconnect = $true } else { Write-Verbose "Already connected to Microsoft Graph as $($contextInfo.Account) with all required scopes." return $contextInfo } } else { # For other auth methods, disconnect and reconnect with the new credentials Write-Verbose "Switching to $AuthMethod authentication method..." Disconnect-MgGraph -ErrorAction SilentlyContinue $reconnect = $true } } else { $reconnect = $true } if ($reconnect) { try { # Define connection parameters based on authentication method switch ($AuthMethod) { "Interactive" { Write-Verbose "Connecting to Microsoft Graph using interactive authentication..." Connect-MgGraph -Scopes $RequiredScopes -NoWelcome } "ClientSecret" { Write-Verbose "Connecting to Microsoft Graph using client secret authentication..." $secureSecret = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force $clientSecretCredential = New-Object System.Management.Automation.PSCredential($ClientId, $secureSecret) Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $clientSecretCredential -NoWelcome } "Certificate" { Write-Verbose "Connecting to Microsoft Graph using certificate authentication..." Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome } "Identity" { Write-Verbose "Connecting to Microsoft Graph using managed identity authentication..." if ($TenantId) { Connect-MgGraph -Identity -TenantId $TenantId -NoWelcome } else { Connect-MgGraph -Identity -NoWelcome } } "AccessToken" { Write-Verbose "Connecting to Microsoft Graph using provided access token..." Connect-MgGraph -AccessToken $AccessToken -TenantId $TenantId -NoWelcome } } # Verify connection $newContext = Get-MgContext if ($newContext) { $authDetails = switch ($AuthMethod) { "Interactive" { "as $($newContext.Account)" } "ClientSecret" { "using client secret (App ID: $ClientId)" } "Certificate" { "using certificate authentication (App ID: $ClientId)" } "Identity" { "using managed identity" } "AccessToken" { "using provided access token" } } Write-Verbose "Successfully connected to Microsoft Graph $authDetails" return $newContext } else { throw "Connection attempt completed but unable to confirm connection" } } catch { Write-Error "Error connecting to Microsoft Graph: $_" return $null } } return $contextInfo } function Get-SecurityGroups { param ( [switch]$Verbose ) $securityGroups = Invoke-GraphRequestWithPaging -Uri "beta/groups?`$filter=isassignabletorole eq true" -Method Get if ($Verbose) { Write-Verbose "Found $($securityGroups.Count) security groups that are assignable to roles" } Else { Write-Host "INFO: Found $($securityGroups.Count) security groups that are assignable to roles" } # Collect members for each security group for later reference $securityGroupMembers = @{} foreach ($group in $securityGroups) { if ($Verbose) { Write-Verbose "Collecting members for security group: $($group.displayName)" } Else { Write-Host "INFO: Collecting members for security group: $($group.displayName)" } try { $members = Invoke-GraphRequestWithPaging -Uri "beta/groups/$($group.id)/transitiveMembers?`$select=id,displayName,userPrincipalName" -Method Get # Create member list with useful information $memberList = @() foreach ($member in $members) { # Handle different member types if ($member.'@odata.type' -eq '#microsoft.graph.user') { $memberList += [PSCustomObject]@{ Type = "User" Id = $member.id DisplayName = $member.displayName UserPrincipalName = $member.userPrincipalName } } Elseif ($member.'@odata.type' -eq '#microsoft.graph.group') { $memberList += [PSCustomObject]@{ Type = "Group" Id = $member.id DisplayName = $member.displayName } } Else { $memberList += [PSCustomObject]@{ Type = $member.'@odata.type'.Replace('#microsoft.graph.', '') Id = $member.id DisplayName = $member.displayName } } } # Store the members in the hashtable $securityGroupMembers[$group.id] = @{ GroupDisplayName = $group.displayName GroupId = $group.id Members = $memberList MemberCount = $memberList.Count } } catch { Write-Error "ERROR: Collecting members for group $($group.displayName): $_" continue } } # Return the security group members return $securityGroupMembers } Function Get-PIMAuditLogs { # Get PIM audit logs $PIMAudits = Invoke-GraphRequestWithPaging "beta/auditLogs/directoryAudits?`$filter=loggedByService eq 'PIM'" $results = @() foreach ($PIMaudit in $PIMAudits) { # Extract user who initiated the action $initiatedByUser = $null if ($PIMaudit.InitiatedBy.user) { if ($PIMaudit.InitiatedBy.user.userPrincipalName) { $initiatedByUser = $PIMaudit.InitiatedBy.user.userPrincipalName } elseif ($PIMaudit.InitiatedBy.user.displayName) { $initiatedByUser = $PIMaudit.InitiatedBy.user.displayName } else { $initi } } # If not a user, check if it was initiated by an app or service principal if (-not $initiatedByUser) { if ($PIMaudit.InitiatedBy.app) { $initiatedByUser = $PIMaudit.InitiatedBy.app.displayName } elseif ($PIMaudit.InitiatedBy.servicePrincipal) { $initiatedByUser = $PIMaudit.InitiatedBy.servicePrincipal.displayName } else { $initiatedByUser = "Unknown" } } # Determine initiator type $initiatorType = "Unknown" if ($PIMaudit.InitiatedBy.user) { $initiatorType = "User" } elseif ($PIMaudit.InitiatedBy.app) { $initiatorType = "Application" } elseif ($PIMaudit.InitiatedBy.servicePrincipal) { $initiatorType = "Service Principal" } # Get role information $roleResource = $PIMaudit.TargetResources | Where-Object {$_.Type -eq "Role"} $roleName = $roleResource.DisplayName $roleId = $roleResource.id # Extract modified properties information $roleProperties = @() if ($roleResource.modifiedProperties) { foreach ($prop in $roleResource.modifiedProperties) { # Clean up the values $oldValue = $prop.oldValue -replace "^'|'$", "" $newValue = $prop.newValue -replace "^'|'$", "" # Make property name more readable $propName = $prop.displayName # Handle common PIM property names switch -Wildcard ($propName) { "*ExpirationTime*" { $propName = "Expiration" } "*ActivationTime*" { $propName = "Activation" } "*StartTime*" { $propName = "Start Time" } "*Justification*" { $propName = "Reason" } "*MemberType*" { $propName = "Member Type" } "*AssignmentState*" { $propName = "Assignment" } } # Format datetime values as before... # For empty values if ([string]::IsNullOrWhiteSpace($oldValue)) { $oldValue = "(none)" } if ([string]::IsNullOrWhiteSpace($newValue)) { $newValue = "(none)" } # Create property change format - using HTML entity for arrow instead of Unicode if ($oldValue -eq "(none)" -and $newValue -ne "(none)") { $roleProperties += "$($propName): $newValue" } elseif ($oldValue -ne "(none)" -and $newValue -eq "(none)") { $roleProperties += "$($propName): Removed" } elseif ($oldValue -eq $newValue) { $roleProperties += "$($propName): $newValue" } else { # Use HTML arrow entity instead of Unicode character $roleProperties += "$($propName): $oldValue → $newValue" } } } $rolePropertiesText = $roleProperties -join " | " # Get request information $requestResource = $PIMaudit.TargetResources | Where-Object {$_.type -eq "Request"} $requestId = $requestResource.id # Extract target user details $userDetails = "N/A" # Check target resources for user $userResource = $PIMaudit.TargetResources | Where-Object {$_.type -eq "User"} if ($userResource -and $userResource.userPrincipalName) { $userDetails = $userResource.userPrincipalName } # Get directory information $directoryResource = $PIMaudit.TargetResources | Where-Object {$_.type -eq "Directory"} $directoryName = $directoryResource.displayName # Get other resources information $otherResource = $PIMaudit.TargetResources | Where-Object {$_.type -eq "Other"} $otherResourceId = $otherResource.id # Get reason for the action $reason = $PIMaudit.ResultReason if ([string]::IsNullOrWhiteSpace($reason)) { $reason = "N/A" } # Extract start time $startTime = "N/A" $startTimeDetail = $PIMaudit.AdditionalDetails | Where-Object {$_.key -eq "StartTime"} if ($startTimeDetail) { try { $parsedStartTime = [DateTime]$startTimeDetail.value $startTime = $parsedStartTime.ToString('dd/MM/yyyy hh:mm:ss tt') } catch { $startTime = $startTimeDetail.value } } # Create custom object with the information $results += [PSCustomObject]@{ "DateTime" = $PIMaudit.ActivityDateTime # "Start Time" = $startTime "InitiatedBy" = $initiatedByUser "OperationType" = $PIMaudit.OperationType "InitiatedByType" = $initiatorType "Role" = $roleName "RoleID" = $roleId "RoleProperties" = $rolePropertiesText "Target" = $userDetails "Directory" = $directoryName # "RequestID" = $requestId # "OtherResourceID" = $otherResourceId "Operation" = $PIMaudit.ActivityDisplayName "Result" = $PIMaudit.Result "Justification" = $reason } } # Sort the results by DateTime in descending order $results = $results | Sort-Object -Property DateTime -Descending # Return the results return $results } #Determine the required scopes, based on the parameters passed to the script $RequiredScopes = @("Directory.Read.All", "PrivilegedEligibilitySchedule.Read.AzureADGroup","Organization.Read.All, AuditLog.Read.All","RoleManagement.Read.Directory") #Connect to the Graph API Write-Verbose "Connecting to Graph API..." try { # Determine which authentication method to use based on parameters $connectionParams = @{ RequiredScopes = $RequiredScopes Verbose = $VerbosePreference -eq 'Continue' } # Add parameters based on which ones were provided if ($PSCmdlet.ParameterSetName -eq "Interactive") { if ($TenantId) { $connectionParams.TenantId = $TenantId } if ($ClientId) { $connectionParams.ClientId = $ClientId } } elseif ($PSCmdlet.ParameterSetName -eq "ClientSecret") { $connectionParams.TenantId = $TenantId $connectionParams.ClientId = $ClientId $connectionParams.ClientSecret = $ClientSecret } elseif ($PSCmdlet.ParameterSetName -eq "Certificate") { $connectionParams.TenantId = $TenantId $connectionParams.ClientId = $ClientId $connectionParams.CertificateThumbprint = $CertificateThumbprint } elseif ($PSCmdlet.ParameterSetName -eq "Identity") { $connectionParams.Identity = $true if ($TenantId) { $connectionParams.TenantId = $TenantId } } elseif ($PSCmdlet.ParameterSetName -eq "AccessToken") { $connectionParams.AccessToken = $AccessToken $connectionParams.TenantId = $TenantId } $connected = Connect-ToMgGraph @connectionParams if ($connected) { Write-Verbose "Connected to Graph API successfully." # Get Tenant Name $tenantInfo = Invoke-MgGraphRequest -Uri "beta/organization" -Method Get -OutputType PSObject $tenantname = $tenantInfo.value[0].displayName # Call the function to get the security group members $securityGroupMembers = Get-SecurityGroups $adminUnits = Invoke-GraphRequestWithPaging -Uri "beta/directory/administrativeUnits" -Method Get $auLookup = @{} foreach ($au in $adminUnits) { # The directoryScopeId format is: "/administrativeUnits/{id}" $auId = "/administrativeUnits/$($au.id)" $auLookup[$auId] = $au.displayName } if ($adminUnits) { Write-Host "INFO: Found administrative units in the tenant." } else { Write-Host "INFO: No administrative units found in the tenant." } # Get role assignments with principal expansion $rolesWithPrincipal = Invoke-GraphRequestWithPaging -Uri "beta/roleManagement/directory/roleAssignments?`$expand=principal" -Method Get # Get role assignments with roleDefinition expansion $rolesWithDefinition = Invoke-GraphRequestWithPaging -Uri "beta/roleManagement/directory/roleAssignments?`$expand=roleDefinition" -Method Get # Merge the data for complete role assignment information $roles = @() foreach ($role in $rolesWithPrincipal) { $roleDefinition = $rolesWithDefinition | Where-Object { $_.id -eq $role.id } | Select-Object -ExpandProperty roleDefinition $role | Add-Member -MemberType NoteProperty -Name roleDefinition1 -Value $roleDefinition -Force $roles += $role } Write-Host "INFO: Found $($rolesWithPrincipal.Count) role assignments in the tenant." try { Write-Host "INFO: Collecting PIM eligible role assignments..." -ForegroundColor Cyan $eligibleRoles = Invoke-GraphRequestWithPaging -Uri "beta/roleManagement/directory/roleEligibilitySchedules?`$expand=roleDefinition,principal" -Method Get if ($null -eq $eligibleRoles) { Write-Warning "Unable to collect PIM eligible role assignments. This may be due to missing Microsoft Entra ID Premium P2 license." Write-Host "INFO: Continuing without PIM eligible role assignments..." -ForegroundColor Yellow $eligibleRoles = @() # Set to empty array so the code can continue } } catch { Write-Warning "Unable to collect PIM eligible role assignments. This may be due to missing Microsoft Entra ID Premium P2 license." Write-Host "INFO: Continuing without PIM eligible role assignments..." -ForegroundColor Yellow $eligibleRoles = @() # Set to empty array so the code can continue } foreach ($eligibleRole in $eligibleRoles) { $eligibleRole | Add-Member -MemberType NoteProperty -Name roleDefinition1 -Value $eligibleRole.roleDefinition -Force $roles += $eligibleRole } if (!$roles) { if ($verbose) { Write-Verbose "No role assignments found, exiting..." } else { Write-Host "INFO: No role assignments found" -ForegroundColor Red } return } $Report = @() foreach ($role in $roles) { # Decide the principal type based on the '@odata.type' property Switch ($role.principal.'@odata.type') { '#microsoft.graph.user' { $principalType = "User" $Principal = $role.principal.userPrincipalName if ($role.principal.accountEnabled -eq $null) { $AccountStatus = "N/A" } elseif ($role.principal.accountEnabled -eq $true) { $AccountStatus = "Enabled" } else { $AccountStatus = "Disabled" } } '#microsoft.graph.group' { $principalType = "Group" $Principal = $role.principal.id if ($role.principal.accountEnabled -eq $null) { $AccountStatus = "N/A" } elseif ($role.principal.accountEnabled -eq $true) { $AccountStatus = "Enabled" } else { $AccountStatus = "Disabled" } } '#microsoft.graph.servicePrincipal' { $principalType = "Service Principal" $Principal = $role.principal.id if ($role.principal.accountEnabled -eq $null) { $AccountStatus = "N/A" } elseif ($role.principal.accountEnabled -eq $true) { $AccountStatus = "Enabled" } else { $AccountStatus = "Disabled" } } } # Decide the role assignment type based on the role eligibility schedule if ($Role.status) { $status = "Eligible" # $tartdate = ($role.scheduleInfo.startDateTime).ToString("dd-MM-yyyy HH:mm") if ($role.scheduleInfo.startDateTime) { $startDate = ($role.scheduleInfo.startDateTime).ToString("dd-MM-yyyy HH:mm") } else { $startDate = "Permanent" } if ($role.scheduleInfo.expiration.endDateTime) { $tartdate = ($role.scheduleInfo.startDateTime).ToString("dd-MM-yyyy HH:mm") $endDate = ($role.scheduleInfo.expiration.endDateTime).ToString("dd-MM-yyyy HH:mm") } else { $endDate = "Permanent" } } else { $status = "Permanent" $StartDate = "Permanent" $endDate = "Permanent" } # Check Assigned Role if ($role.roleDefinition1.displayName) { $assignedRole = $role.roleDefinition1.displayName } elseif ($role.roleDefinition.displayName) { $assignedRole = $role.roleDefinition.displayName } else { $assignedRole = "Unknown" } $Reportline = [PSCustomObject]@{ "Principal" = $Principal "DisplayName" = $role.principal.displayName "AccountStatus" = $AccountStatus "PrincipalType" = $principalType "Assigned Role" = $assignedRole "AssignedRoleScopeName" = if ($role.directoryScopeId -eq "/" -or $null -eq $role.directoryScopeId) { "Tenant-Wide" } else { "AU/$($auLookup[$role.directoryScopeId])" } "AssignmentType" = $status "AssignmentStartDate" = $startDate "AssignmentEndDate" = $endDate #"Members" = if ($principalType -eq "Group") { $securityGroupMembers[$role.principal.id].Members } else { $null } "IsBuiltIn" = if ($role.roleDefinition.isBuiltIn) { $role.roleDefinition.isBuiltIn } elseif ($role.roleDefinition1.isBuiltIn) { $role.roleDefinition1.isBuiltIn } else { $null } } $report += $Reportline } # Collect subset of roles for each principal type $GroupAssignmentReport = $report | Where-Object { $_.PrincipalType -eq "group" } $ServicePrincipalReport = $report | Where-Object { $_.PrincipalType -eq "service Principal" } | Select-Object -ExcludeProperty Members $UserAssignmentReport = $report | Where-Object { $_.PrincipalType -eq "user" } | Select-Object -ExcludeProperty Members # Create a summary of the report $GroupMembershipOverviewReport = @() foreach ($group in $GroupAssignmentReport) { # Look up group members from our previously collected data $members = ($securityGroupMembers.Values | Where-Object { $_.groupid -eq $group.Principal }).members.userprincipalname -join ", " if (-not $members) { $members = "None" } else { $members = $members -join ", " } $Reportline = [PSCustomObject]@{ Principal = $group.Principal DisplayName = $group.DisplayName Members = $members } $GroupMembershipOverviewReport += $Reportline } $GroupMembershipOverviewReport = $GroupMembershipOverviewReport | Select-Object -Property Principal, DisplayName, Members -Unique # Get PIM audit logs $PIMAuditLogsReport = Get-PIMAuditLogs if ($PIMAuditLogsReport) { Write-Host "INFO: Found $($PIMAuditLogsReport.Count) PIM audit logs." -ForegroundColor Green } else { Write-Host "INFO: No PIM audit logs found." -ForegroundColor Yellow } New-AdminRoleHTMLReport -TenantName $tenantname -Report $Report -UserAssignmentReport $UserAssignmentReport -GroupAssignmentReport $GroupAssignmentReport -ServicePrincipalReport $ServicePrincipalReport -GroupMembershipOverviewReport $GroupMembershipOverviewReport -PIMAuditLogsReport $PIMAuditLogsReport } else { throw "Failed to connect to Microsoft Graph API." } } catch { Write-Error "Error: $_" throw $_ } finally { # Disconnect from Microsoft Graph if connected $contextInfo = Get-MgContext -ErrorAction SilentlyContinue if ($contextInfo) { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Write-Host "Disconnected from Microsoft Graph." -ForegroundColor Green } else { Write-Host "No active connection to disconnect." -ForegroundColor Yellow } } |