IntuneRBAC.ps1
<#PSScriptInfo
.VERSION 0.2.3 .GUID 552abbe1-5543-41a3-bd39-eab7613593f2 .AUTHOR ugurk .COMPANYNAME .COPYRIGHT Copyright (c) 2025 Ugur Koc | Microsoft MVP .TAGS Intune RBAC RoleBasedAccessControl ScopeTags Permissions .LICENSEURI https://github.com/ugurkocde/IntuneRBAC/blob/main/LICENSE .PROJECTURI https://github.com/ugurkocde/IntuneRBAC .ICONURI .EXTERNALMODULEDEPENDENCIES Microsoft.Graph.Authentication .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES Version 0.2.3: Current version with RBAC health check functionality. Version 0.2.2: Added interactive Role Relationship Diagram. Version 0.2.1: Added comprehensive Permissions Matrix. Version 0.2.0: Added security analysis for unused roles and overlapping permissions. Version 0.1.0: Initial release with basic RBAC reporting capabilities. .PRIVATEDATA #> <# .DESCRIPTION This script provides a comprehensive analysis of Microsoft Intune's Role-Based Access Control (RBAC) configuration. It generates an interactive HTML report that includes role details, assignments, scope tags, permissions, and security analysis to help administrators audit and manage their Intune RBAC setup. #> #Requires -Version 7.0 # Step 1: Connect to Microsoft Graph Connect-MgGraph -Scopes "DeviceManagementRBAC.Read.All, DeviceManagementApps.Read.All, DeviceManagementConfiguration.Read.All, User.ReadBasic.All" -NoWelcome # Get tenant information and timestamp $tenantInfo = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/organization" -Method GET $tenantName = $tenantInfo.value[0].displayName $lastUpdated = Get-Date -Format "MMMM dd, yyyy HH:mm" $version = "0.2.3" # Data processing for charts $rolesWithScopeTagsCount = 0 $rolesWithoutScopeTagsCount = 0 $customRolesCount = 0 $builtInRolesCount = 0 $unusedRolesCount = 0 $rolesWithOverlappingPermissionsCount = 0 $script:allPermissionsMatrixData = @{} $script:allRoleNamesForMatrixData = [System.Collections.Generic.List[string]]::new() $script:graphNodes = [System.Collections.Generic.List[object]]::new() $script:graphLinks = [System.Collections.Generic.List[object]]::new() $script:processedGraphNodeIds = [System.Collections.Generic.HashSet[string]]::new() # Fetch all roles first $rolesUri = "https://graph.microsoft.com/beta/deviceManagement/roleDefinitions" $response = Invoke-MgGraphRequest -Uri $rolesUri -Method GET # Process the roles for counting foreach ($role in $response.value) { if ($role.roleScopeTagIds.Count -gt 0) { $rolesWithScopeTagsCount++ } else { $rolesWithoutScopeTagsCount++ } if ($role.isBuiltIn) { $builtInRolesCount++ } else { $customRolesCount++ } } function Get-RoleAssignments { param($roleId) $assignmentsUri = "https://graph.microsoft.com/beta/deviceManagement/roleDefinitions('$roleId')/roleAssignments" $response = Invoke-MgGraphRequest -Uri $assignmentsUri -Method GET $assignments = @() foreach ($assignment in $response.value) { $assignments += [PSCustomObject]@{ DisplayName = $assignment.displayName RoleDefinitionId = $assignment.id } } if ($response.'@odata.nextLink') { $assignments += Get-RoleAssignments -roleId $roleId } return $assignments } function Get-RoleMembers { param($roleDefinitionId) $membersUri = "https://graph.microsoft.com/beta/deviceManagement/roleAssignments('$roleDefinitionId')`?$expand=microsoft.graph.deviceAndAppManagementRoleAssignment/roleScopeTags" $response = Invoke-MgGraphRequest -Uri $membersUri -Method GET $members = @() foreach ($member in $response) { $groupId = $member.members -join ", " # Assuming there's only one group per member # Fetch the group name $groupUri = "https://graph.microsoft.com/beta/groups/$groupId" $groupResponse = Invoke-MgGraphRequest -Uri $groupUri -Method GET $groupName = $groupResponse.displayName $members += [PSCustomObject]@{ RoleAssignmentName = $member.displayName RoleAssignmentId = $member.id GroupId = $groupId GroupName = $groupName } } return $members } function Get-GroupMembers { param($groupId) $groupMembersUri = "https://graph.microsoft.com/beta/groups/$groupId/members" $response = Invoke-MgGraphRequest -Uri $groupMembersUri -Method GET $userIds = @() foreach ($member in $response.value) { if ($member.id) { $userIds += $member.id } } # Check for pagination if ($response.'@odata.nextLink') { $userIds += Get-GroupMembers -groupId $groupId } $upns = @() foreach ($userId in $userIds) { $userUri = "https://graph.microsoft.com/beta/users/$userId" $userResponse = Invoke-MgGraphRequest -Uri $userUri -Method GET if ($userResponse.userPrincipalName) { $upns += $userResponse.userPrincipalName } } return $upns } function Get-ScopeTags { param($Uri) $response = Invoke-MgGraphRequest -Uri $Uri -Method GET $scopeTags = @{} foreach ($tag in $response.value) { $scopeTags[$tag.id] = @{ DisplayName = $tag.displayName Description = $tag.description } } if ($response.'@odata.nextLink') { $scopeTags += Get-ScopeTags -Uri $response.'@odata.nextLink' } return $scopeTags } # Function to categorize permissions function Get-CategorizedPermissions { param($actions) $categories = @{ 'Mobile Apps' = @() 'Managed Apps' = @() 'Devices' = @() 'Policies' = @() 'Filters' = @() 'Security' = @() 'Other' = @() 'Cloud Attach' = @() } foreach ($action in $actions) { switch -Wildcard ($action) { "Microsoft.Intune_MobileApps_*" { $categories['Mobile Apps'] += $action.Replace("Microsoft.Intune_", "") } "Microsoft.Intune_ManagedApps_*" { $categories['Managed Apps'] += $action.Replace("Microsoft.Intune_", "") } "Microsoft.Intune_Devices_*" { $categories['Devices'] += $action.Replace("Microsoft.Intune_", "") } "Microsoft.Intune_DeviceConfigurations_*" { $categories['Policies'] += $action.Replace("Microsoft.Intune_", "") } "Microsoft.Intune_Filter_*" { $categories['Filters'] += $action.Replace("Microsoft.Intune_", "") } "Microsoft.Intune_Security*" { $categories['Security'] += $action.Replace("Microsoft.Intune_", "") } "Microsoft.Intune_CloudAttach_*" { $categories['Cloud Attach'] += $action.Replace("Microsoft.Intune_", "") } default { $categories['Other'] += $action.Replace("Microsoft.Intune_", "") } } } return $categories } # Function to check for unused roles function Test-UnusedRole { param($roleId) $assignments = Get-RoleAssignments -roleId $roleId return $assignments.Count -eq 0 } # Function to find overlapping permissions between roles function Get-OverlappingPermissions { param($allRoles) $overlaps = @{} # Create a lookup of role ID to permissions $rolePermissions = @{} foreach ($role in $allRoles) { $allowedActions = @() foreach ($perm in $role.rolePermissions) { foreach ($action in $perm.resourceActions) { $allowedActions += $action.allowedResourceActions } } $rolePermissions[$role.id] = @{ DisplayName = $role.displayName Permissions = $allowedActions } } # Compare each role with every other role foreach ($roleId in $rolePermissions.Keys) { $overlaps[$roleId] = @{} foreach ($otherRoleId in $rolePermissions.Keys) { if ($roleId -ne $otherRoleId) { $commonPermissions = Compare-Object -ReferenceObject $rolePermissions[$roleId].Permissions -DifferenceObject $rolePermissions[$otherRoleId].Permissions -IncludeEqual | Where-Object { $_.SideIndicator -eq '==' } | Select-Object -ExpandProperty InputObject if ($commonPermissions.Count -gt 0) { $overlaps[$roleId][$otherRoleId] = @{ RoleName = $rolePermissions[$otherRoleId].DisplayName CommonPermissions = $commonPermissions OverlapPercentage = [math]::Round(($commonPermissions.Count / $rolePermissions[$roleId].Permissions.Count) * 100, 1) } } } } } return $overlaps } # Function to Fetch Roles and their Scope Tags function Get-RolesWithScopeTags { param( $Uri, $ScopeTags ) $response = Invoke-MgGraphRequest -Uri $Uri -Method GET # Store all roles for overlap analysis $allRoles = $response.value # Get overlapping permissions $overlappingPermissions = Get-OverlappingPermissions -allRoles $allRoles $htmlContent = @() foreach ($role in $allRoles) { $allowedActions = @() foreach ($perm in $role.rolePermissions) { foreach ($action in $perm.resourceActions) { $allowedActions += $action.allowedResourceActions } } # ---- START: Populate data for Permissions Matrix ---- if (-not $script:allRoleNamesForMatrixData.Contains($role.displayName)) { $script:allRoleNamesForMatrixData.Add($role.displayName) # When a new role is added, existing permissions in the matrix need an entry for this new role, defaulting to false foreach ($existingPermName in $script:allPermissionsMatrixData.Keys) { if (-not $script:allPermissionsMatrixData[$existingPermName].ContainsKey($role.displayName)) { $script:allPermissionsMatrixData[$existingPermName][$role.displayName] = $false } } } foreach ($permissionString in $allowedActions) { $cleanPermissionName = $permissionString.Replace("Microsoft.Intune_", "") if (-not $script:allPermissionsMatrixData.ContainsKey($cleanPermissionName)) { $script:allPermissionsMatrixData[$cleanPermissionName] = @{} # For a new permission, initialize for all known roles with false foreach ($knownRoleName in $script:allRoleNamesForMatrixData) { if (-not $script:allPermissionsMatrixData[$cleanPermissionName].ContainsKey($knownRoleName)) { $script:allPermissionsMatrixData[$cleanPermissionName][$knownRoleName] = $false } } } # Ensure the current role has an entry for this permission if (-not $script:allPermissionsMatrixData[$cleanPermissionName].ContainsKey($role.displayName)) { $script:allPermissionsMatrixData[$cleanPermissionName][$role.displayName] = $false # Initialize if somehow missed } $script:allPermissionsMatrixData[$cleanPermissionName][$role.displayName] = $true } # ---- END: Populate data for Permissions Matrix ---- # Security Analysis $isUnused = Test-UnusedRole -roleId $role.id $hasOverlappingPermissions = $overlappingPermissions[$role.id].Count -gt 0 # Update counters if ($isUnused) { $script:unusedRolesCount++ } if ($hasOverlappingPermissions) { $script:rolesWithOverlappingPermissionsCount++ } $roleType = if ($role.isBuiltIn) { "Built-In Role" } else { "Custom Role" } # Format scope tag information $scopeTagInfo = "" if ($role.roleScopeTagIds.Count -gt 0) { $scopeTags = @() foreach ($tagId in $role.roleScopeTagIds) { $tagDetails = $ScopeTags[$tagId] $scopeTags += $tagDetails.DisplayName } $scopeTagInfo = "<div class='scope-tag'><strong>Scope Tag:</strong> $($scopeTags -join ', ')</div>" } else { $scopeTagInfo = "<div class='no-scope-tag'>No Scope Tag assigned</div>" } # Create security badges $securityBadges = "<div class='accordion-badges'>" if ($isUnused) { $securityBadges += "<span class='security-badge warning'><i class='fas fa-exclamation-triangle'></i> Unused Role</span>" } if ($hasOverlappingPermissions) { $securityBadges += "<span class='security-badge info'><i class='fas fa-info-circle'></i> Overlapping Permissions</span>" } $securityBadges += "</div>" # Start the accordion for each role $htmlContent += "<button class='accordion'><div class='accordion-header'><span class='accordion-title'>$($role.displayName)</span>$securityBadges</div></button>" $htmlContent += "<div class='panel'>" $htmlContent += "<div class='panel-content'>" # Top Panel with Basic Info and Role Assignments side by side $htmlContent += "<div class='panel-top'>" # Basic Info Section $htmlContent += "<div class='panel-top-section'>" $htmlContent += "<h3><i class='fas fa-info-circle'></i>Basic Information</h3>" $htmlContent += "<p><strong>Description:</strong> $($role.description)</p>" $htmlContent += "<p><strong>Type:</strong> $roleType</p>" $htmlContent += $scopeTagInfo $htmlContent += "</div>" # Role Assignment Section (if exists) $roleAssignments = Get-RoleAssignments -roleId $role.id if ($roleAssignments) { $htmlContent += "<div class='panel-top-section'>" $htmlContent += "<h3><i class='fas fa-users'></i>Role Assignments</h3>" foreach ($assignment in $roleAssignments) { $roleMembers = Get-RoleMembers -roleDefinitionId $assignment.RoleDefinitionId foreach ($member in $roleMembers) { $groupMembers = Get-GroupMembers -groupId $member.GroupId $upns = $groupMembers -join ", " $htmlContent += "<p><strong>Assignment:</strong> $($member.RoleAssignmentName)</p>" $htmlContent += "<p><strong>Group:</strong> $($member.GroupName)</p>" $htmlContent += "<p><strong>Members:</strong> $upns</p>" } } $htmlContent += "</div>" } else { $htmlContent += "<div class='panel-top-section warning-section'>" $htmlContent += "<h3><i class='fas fa-exclamation-triangle'></i>Unused Role</h3>" $htmlContent += "<p>This role is not assigned to any groups or users.</p>" $htmlContent += "<p>Consider removing this role if it's not needed or assign it to appropriate groups.</p>" $htmlContent += "</div>" } $htmlContent += "</div>" # Close panel-top # Security Analysis Section (Only show if overlaps exist) if ($hasOverlappingPermissions) { $htmlContent += "<div class='security-analysis'>" $htmlContent += "<h3><i class='fas fa-shield-alt'></i>Security Analysis</h3>" # Overlapping Permissions if ($hasOverlappingPermissions) { $htmlContent += "<div class='security-section info-section'>" $htmlContent += "<h4><i class='fas fa-info-circle'></i>Overlapping Permissions</h4>" $htmlContent += "<p>This role has significant permission overlap with the following roles:</p>" $htmlContent += "<ul class='overlap-list'>" # Get top 3 overlapping roles by percentage $topOverlaps = $overlappingPermissions[$role.id].GetEnumerator() | Sort-Object { $_.Value.OverlapPercentage } -Descending | Select-Object -First 3 foreach ($overlap in $topOverlaps) { $htmlContent += "<li><strong>$($overlap.Value.RoleName):</strong> $($overlap.Value.OverlapPercentage)% overlap ($($overlap.Value.CommonPermissions.Count) permissions)</li>" } $htmlContent += "</ul>" $htmlContent += "</div>" } $htmlContent += "</div>" # Close security-analysis } # Bottom Panel (Resource Actions) $htmlContent += "<div class='panel-bottom'>" if ($allowedActions) { $categories = Get-CategorizedPermissions -actions $allowedActions $totalPermissions = ($allowedActions | Measure-Object).Count $categoryCount = ($categories.Keys | Where-Object { $categories[$_].Count -gt 0 } | Measure-Object).Count $htmlContent += "<div class='resource-actions'>" $htmlContent += "<div class='resource-actions-header'>" $htmlContent += "<div class='resource-actions-title'>" $htmlContent += "<h3><i class='fas fa-shield-alt'></i>Allowed Resource Actions</h3>" $htmlContent += "<span class='resource-actions-count'>This role has $totalPermissions permissions across $categoryCount categories</span>" $htmlContent += "</div>" $htmlContent += "<input type='text' class='permission-search' placeholder='Search permissions...' onkeyup='filterPermissions(this)'>" $htmlContent += "</div>" # Tabs $htmlContent += "<div class='permission-tabs'>" $htmlContent += "<button class='permission-tab active' onclick='showCategory(this, `"all`")'>All Permissions</button>" foreach ($category in $categories.Keys | Where-Object { $categories[$_].Count -gt 0 }) { $htmlContent += "<button class='permission-tab' onclick='showCategory(this, `"$category`")'>$category</button>" } $htmlContent += "</div>" # Categories foreach ($category in $categories.Keys) { if ($categories[$category].Count -gt 0) { $htmlContent += "<div class='permission-category' data-category='$category'>" $htmlContent += "<div class='category-header'>" $htmlContent += "<span class='category-title'>$category</span>" $htmlContent += "<span class='category-count'>$($categories[$category].Count)</span>" $htmlContent += "</div>" $htmlContent += "<div class='permission-list'>" foreach ($permission in $categories[$category]) { $htmlContent += "<div class='permission-item'>" $htmlContent += "<span class='permission-icon'></span>" $htmlContent += "<span class='permission-name'>$permission</span>" $htmlContent += "</div>" } $htmlContent += "</div>" $htmlContent += "</div>" } } $htmlContent += "</div>" # Close resource-actions } $htmlContent += "</div>" # Close panel-bottom $htmlContent += "</div>" # Close panel-content $htmlContent += "</div>" # Close panel # ---- START: Populate data for Interactive Role Relationship Diagram ---- try { # Add Role Node if ($script:processedGraphNodeIds.Add($role.id)) { $script:graphNodes.Add(@{ id = $role.id label = $role.displayName type = "role" title = "Role: $($role.displayName)<br>Built-in: $($role.isBuiltIn)<br>Permissions: $($allowedActions.Count)" group = "role" # For vis.js styling builtin = $role.isBuiltIn # Store for tooltip or other logic permissionsCount = $allowedActions.Count # Store for tooltip }) } # Get assignments for this role $roleAssignmentsForGraph = Get-RoleAssignments -roleId $role.id if ($roleAssignmentsForGraph) { foreach ($assignmentItem in $roleAssignmentsForGraph) { # $assignmentItem.RoleDefinitionId is actually the RoleAssignmentId here based on Get-RoleAssignments structure $roleMembersForGraph = Get-RoleMembers -roleDefinitionId $assignmentItem.RoleDefinitionId if ($roleMembersForGraph) { foreach ($groupMemberItem in $roleMembersForGraph) { # Add Group Node if ($script:processedGraphNodeIds.Add($groupMemberItem.GroupId)) { $script:graphNodes.Add(@{ id = $groupMemberItem.GroupId label = $groupMemberItem.GroupName type = "group" title = "Group: $($groupMemberItem.GroupName)" group = "group" }) } # Add Role-to-Group Link $script:graphLinks.Add(@{ from = $role.id to = $groupMemberItem.GroupId type = "role_to_group" # For styling/filtering }) # Get users in this group $userUpnsInGroup = Get-GroupMembers -groupId $groupMemberItem.GroupId if ($userUpnsInGroup) { foreach ($userUpn in $userUpnsInGroup) { # Add User Node (use UPN as ID for users for simplicity, ensure it's unique) $userIdForGraph = "user_" + $userUpn # Prefix to avoid collision with other IDs if ($script:processedGraphNodeIds.Add($userIdForGraph)) { $script:graphNodes.Add(@{ id = $userIdForGraph label = $userUpn.Split('@')[0] # Display username part type = "user" title = "User: $($userUpn)" group = "user" fullUpn = $userUpn }) } # Add Group-to-User Link $script:graphLinks.Add(@{ from = $groupMemberItem.GroupId to = $userIdForGraph type = "group_to_user" }) } } } } } } } catch { Write-Warning "Error populating graph data for role $($role.displayName): $($_.Exception.Message)" } # ---- END: Populate data for Interactive Role Relationship Diagram ---- } # Final pass to ensure matrix is complete and sort role names foreach ($permNameKey in $script:allPermissionsMatrixData.Keys) { foreach ($roleNameKey in $script:allRoleNamesForMatrixData) { if (-not $script:allPermissionsMatrixData[$permNameKey].ContainsKey($roleNameKey)) { $script:allPermissionsMatrixData[$permNameKey][$roleNameKey] = $false } } } $script:allRoleNamesForMatrixData.Sort() # Sort roles once after all are collected and matrix is finalized return $htmlContent } function Generate-PermissionsMatrixHtml { $matrixHtml = @() $matrixHtml += "<h2 id='permissions-matrix-section'><i class='fas fa-table'></i> Permissions Matrix</h2>" $matrixHtml += "<div class='permissions-matrix-container'>" # ID moved to H2 for direct navigation $matrixHtml += "<table class='permissions-matrix-table'>" $matrixHtml += "<thead><tr><th>Permission</th>" # Role names are already sorted in $script:allRoleNamesForMatrixData by Get-RolesWithScopeTags foreach ($roleName in $script:allRoleNamesForMatrixData) { $matrixHtml += "<th>$($roleName)</th>" } $matrixHtml += "</tr></thead>" $matrixHtml += "<tbody>" # Sort permission names for row display $sortedPermissionNames = $script:allPermissionsMatrixData.Keys | Sort-Object foreach ($permissionName in $sortedPermissionNames) { $matrixHtml += "<tr><td>$($permissionName)</td>" # Permission names are already cleaned foreach ($roleName in $script:allRoleNamesForMatrixData) { # Use the sorted list for column order $hasPermission = $script:allPermissionsMatrixData[$permissionName].ContainsKey($roleName) -and $script:allPermissionsMatrixData[$permissionName][$roleName] $cellContent = if ($hasPermission) { "<span class='permission-check'>✔️</span>" } else { "<span class='permission-no'></span>" } $matrixHtml += "<td>$cellContent</td>" } $matrixHtml += "</tr>" } $matrixHtml += "</tbody></table></div>" return ($matrixHtml -join "`r`n") } # Fetch Scope Tags $scopeTagsUri = "https://graph.microsoft.com/beta/deviceManagement/roleScopeTags" $scopeTags = Get-ScopeTags -Uri $scopeTagsUri # Fetch Roles with Scope Tags $rolesUri = "https://graph.microsoft.com/beta/deviceManagement/roleDefinitions" $htmlRolesWithScopeTags = Get-RolesWithScopeTags -Uri $rolesUri -ScopeTags $scopeTags # Calculate total number of roles $totalRolesCount = $rolesWithScopeTagsCount + $rolesWithoutScopeTagsCount # Get the number of scope tags $scopeTagsCount = $scopeTags.Count # Create HTML file content $navigationButtons = @" <a href="#rbac-statistics-section" class="hero-button"> <i class="fas fa-chart-bar"></i> RBAC Stats </a> <a href="#security-analysis-section" class="hero-button"> <i class="fas fa-shield-alt"></i> Security Analysis </a> <a href="#roles-overview-section" class="hero-button"> <i class="fas fa-user-cog"></i> Roles Overview </a> <a href="#permissions-matrix-section" class="hero-button"> <i class="fas fa-table"></i> Permissions Matrix </a> <a href="#role-relationship-diagram-section" class="hero-button"> <i class="fas fa-project-diagram"></i> Relationship Diagram </a> "@ $htmlHeader = @" <!DOCTYPE html> <html> <head> <title>Intune RBAC Health Check</title> <style> :root { --primary-color: #2D3047; --primary-light: #419D78; --primary-dark: #1A1B2E; --secondary-color: #00B2CA; --secondary-dark: #0899AF; --accent-color: #419D78; --accent-light: #5CBA97; --background-color: #ffffff; --surface-color: #F8FAFC; --text-color: #2D3047; --card-background: #ffffff; --border-color: #E2E8F0; --error-color: #EF476F; --warning-color: #FF9F1C; --warning-rgb: 255, 159, 28; /* RGB for rgba */ --info-color: #2196F3; --info-rgb: 33, 150, 243; /* RGB for rgba */ } body { background-color: var(--background-color); color: var(--text-color); font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; } .hero { background: linear-gradient(135deg, #2D3047 0%, /* Deep Navy */ #419D78 50%, /* Emerald Green */ #00B2CA 100% /* Turquoise */ ); color: white; padding: 20px 40px 100px; /* Increased bottom padding */ position: relative; overflow: hidden; } .hero::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); opacity: 0.07; /* Slightly reduced opacity */ } .hero-content { width: 100%; max-width: 1400px; margin: 0 auto; text-align: center; } .hero-meta { display: flex; justify-content: center; gap: 24px; margin-bottom: 24px; font-size: 0.9em; opacity: 0.9; } .hero-meta-item { display: flex; align-items: center; gap: 8px; padding: 4px 12px; background: rgba(255, 255, 255, 0.1); border-radius: 6px; } .hero-meta-item i { opacity: 0.8; font-size: 14px; } .hero-main { display: flex; flex-direction: column; align-items: center; gap: 24px; } .hero-title-section { text-align: center; } .hero h1 { font-size: 2.5em; margin: 0; padding: 0; border: none; color: white; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); line-height: 1.2; } .hero-subtitle { font-size: 1.1em; margin: 12px 0 0 0; opacity: 0.9; font-weight: 300; text-align: center; } .hero-buttons { display: flex; justify-content: center; gap: 12px; margin-top: 24px; /* Increased top margin */ margin-bottom: 20px; /* Added bottom margin */ flex-wrap: wrap; /* Allow buttons to wrap on smaller screens */ } .hero-button { display: inline-flex; align-items: center; padding: 8px 16px; background-color: rgba(255, 255, 255, 0.15); color: white; text-decoration: none; border-radius: 6px; border: 1px solid rgba(255, 255, 255, 0.3); transition: all 0.2s ease; font-size: 0.9em; gap: 8px; cursor: pointer; position: relative; z-index: 2; } .hero-button:hover { background-color: rgba(255, 255, 255, 0.25); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .hero-button i { font-size: 16px; } .container { max-width: 1200px; margin: 0 auto; padding: 0 10px; } h1 { text-align: center; color: var(--primary-color); font-size: 2.5em; margin-bottom: 40px; padding-bottom: 10px; border-bottom: 2px solid var(--primary-color); } h2 { color: var(--accent-color); font-size: 1.8em; margin-top: 30px; display: flex; /* Added for icon alignment */ align-items: center; gap: 10px; margin-bottom: 15px; /* Added default bottom margin */ } /* Specific spacing for headers within stats container */ .stats-container h2 { margin-bottom: 25px; /* Increased bottom margin */ } #security-analysis-section { /* Target the h2 directly */ margin-top: 45px; /* Increased top margin */ } .stats-container { background-color: var(--surface-color); border-radius: 10px; padding: 30px; margin-top: -80px; /* Adjusted to be smaller than hero's bottom padding */ margin-bottom: 30px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); position: relative; z-index: 1; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); /* Adjusted minmax */ gap: 25px; /* Increased gap */ margin-top: 20px; } .stat-card { background-color: var(--card-background); padding: 25px 20px; /* Adjusted padding */ border-radius: 12px; /* Softer radius */ transition: all 0.3s ease; /* Smoother transition */ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); /* Softer shadow */ border: 1px solid var(--border-color); display: flex; /* Use flexbox */ flex-direction: column; align-items: center; /* Center items horizontally */ justify-content: center; /* Center items vertically */ gap: 5px; /* Reduced gap between elements */ position: relative; /* For potential absolute elements later */ overflow: hidden; /* Hide overflow if needed */ text-align: center; /* Ensure text is centered */ } .stat-card:hover { transform: translateY(-6px); /* Slightly more lift */ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); /* Enhanced shadow on hover */ } .stat-card-icon { font-size: 1.6em; /* Icon size */ color: var(--accent-color); /* Default icon color */ margin-bottom: 10px; /* Space below icon */ line-height: 1; /* Ensure icon aligns well */ } .stat-number { font-size: 2.6em; /* Slightly larger number */ font-weight: 600; /* Slightly less bold */ color: var(--primary-color); margin: 0; /* Remove default margin */ line-height: 1.1; } .stat-label { color: var(--text-color); font-size: 1em; /* Slightly smaller label */ margin-top: 5px; /* Space above label */ line-height: 1.3; } /* Specific icon colors for warning/info cards */ .stat-card.warning .stat-card-icon { color: var(--warning-color); } .stat-card.info .stat-card-icon { color: var(--info-color); } .chart-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; margin: 40px 0; padding: 20px; background-color: var(--surface-color); border-radius: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .canvas-chart { background-color: var(--card-background); padding: 20px; border-radius: 8px; min-height: 300px; border: 1px solid var(--border-color); } .accordion { background-color: var(--surface-color); color: var(--text-color); cursor: pointer; padding: 18px; width: 100%; border: 1px solid var(--border-color); text-align: left; outline: none; font-size: 15px; transition: 0.3s; border-radius: 8px; margin-bottom: 5px; display: block; } .accordion-header { display: flex; align-items: center; width: 100%; justify-content: space-between; } .accordion-title { font-weight: bold; } .accordion-badges { display: flex; gap: 5px; flex-wrap: wrap; justify-content: flex-end; } .security-badge { display: inline-flex; align-items: center; margin-left: 5px; padding: 4px 8px; border-radius: 4px; font-size: 0.8em; font-weight: normal; white-space: nowrap; } .security-badge i { margin-right: 5px; } .security-badge.warning { background-color: var(--warning-color); color: white; } .security-badge.info { background-color: var(--info-color); color: white; } .accordion:after { content: '+'; color: var(--primary-color); font-weight: bold; float: right; margin-left: 5px; font-size: 20px; } .active:after { content: '−'; } .active, .accordion:hover { background-color: var(--card-background); color: var(--primary-color); } .panel { padding: 0; background-color: var(--card-background); max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; border-radius: 0 0 8px 8px; margin-bottom: 10px; border: 1px solid var(--border-color); border-top: none; } .panel.active { padding: 20px; max-height: none; } .panel-content { display: flex; flex-direction: column; gap: 20px; padding: 20px; } .panel-top { display: flex; gap: 20px; } .panel-top-section { flex: 1; background-color: var(--surface-color); padding: 15px; border-radius: 8px; border: 1px solid var(--border-color); } .panel-top-section h3 { display: flex; align-items: center; gap: 10px; margin: 0 0 10px 0; } .panel-bottom { width: 100%; } .scope-tag { color: var(--accent-color); margin-top: 15px; } .no-scope-tag { color: #666; font-style: italic; } .resource-actions { background-color: var(--surface-color); padding: 20px; border-radius: 8px; margin-top: 20px; border: 1px solid var(--border-color); max-height: 600px; overflow-y: auto; } .resource-actions-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .resource-actions-title { display: flex; align-items: center; gap: 10px; } .resource-actions-title h3 { margin: 0; color: var(--text-color); display: flex; align-items: center; gap: 10px; } .resource-actions-title h3 i { margin-right: 5px; } .resource-actions-count { color: var(--text-color); font-size: 0.9em; } .permission-search { padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 4px; width: 250px; font-size: 14px; } .permission-tabs { display: flex; flex-wrap: wrap; gap: 5px; margin: 20px 0; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; } .permission-tab { padding: 8px 16px; border: none; background: none; cursor: pointer; color: var(--text-color); font-size: 14px; border-radius: 4px; transition: all 0.3s; } .permission-tab:hover { background-color: var(--border-color); } .permission-tab.active { background-color: var(--accent-color); color: white; } .permission-category { display: block; margin-top: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--border-color); } .permission-category:last-child { border-bottom: none; } .category-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .category-title { font-size: 1.1em; color: var(--text-color); font-weight: 600; } .category-count { background-color: var(--surface-color); padding: 2px 8px; border-radius: 12px; font-size: 0.9em; color: var(--text-color); } .permission-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; } .permission-item { display: flex; align-items: center; gap: 10px; padding: 8px; background-color: var(--surface-color); border-radius: 4px; border: 1px solid var(--border-color); } .permission-icon { width: 8px; height: 8px; background-color: var(--secondary-color); border-radius: 50%; } .permission-name { font-size: 0.9em; color: var(--text-color); } .security-badge { display: inline-flex; align-items: center; margin-left: 10px; padding: 4px 8px; border-radius: 4px; font-size: 0.8em; font-weight: normal; } .security-badge i { margin-right: 5px; } .security-badge.warning { background-color: var(--warning-color); color: white; } .security-badge.info { background-color: var(--info-color); color: white; } .security-analysis { margin: 20px 0; padding: 20px; background-color: var(--surface-color); border-radius: 8px; border: 1px solid var(--border-color); } .security-analysis h3 { display: flex; align-items: center; gap: 10px; margin-top: 0; margin-bottom: 15px; color: var(--text-color); } .security-section { margin-bottom: 15px; padding: 15px; border-radius: 8px; border: 1px solid var(--border-color); } .warning-section { background-color: rgba(255, 159, 28, 0.1); border-left: 4px solid var(--warning-color); } .info-section { background-color: rgba(33, 150, 243, 0.1); border-left: 4px solid var(--info-color); } .security-section h4 { display: flex; align-items: center; gap: 10px; margin-top: 0; margin-bottom: 10px; } .gap-list, .overlap-list { margin: 10px 0; padding-left: 20px; } .gap-list li, .overlap-list li { margin-bottom: 5px; } .stat-card.warning { border-left: 5px solid var(--warning-color); /* Thicker border */ background-color: rgba(var(--warning-rgb), 0.03); /* Subtle background tint */ } .stat-card.info { border-left: 5px solid var(--info-color); /* Thicker border */ background-color: rgba(var(--info-rgb), 0.03); /* Subtle background tint */ } /* Removed .stat-card.critical as the feature was removed */ @media screen and (max-width: 768px) { border-left: 4px solid var(--error-color); /* Match badge color */ } @media screen and (max-width: 768px) { .panel-top { flex-direction: column; } } .footer { background-color: var(--surface-color); padding: 20px 0; margin-top: 40px; border-top: 1px solid var(--border-color); text-align: center; } .footer-content { max-width: 1200px; margin: 0 auto; padding: 0 20px; } .footer-text { color: var(--text-color); font-size: 1em; margin: 0; } .footer-link { color: var(--accent-color); text-decoration: none; font-weight: 500; transition: color 0.3s ease; } .footer-link:hover { color: var(--accent-light); } /* Styles for Permissions Matrix Table */ .permissions-matrix-container { margin-top: 40px; overflow-x: auto; /* For wide tables */ padding-bottom: 20px; /* Space for horizontal scrollbar if needed */ background-color: var(--background-color); /* Ensure container has a background for sticky elements */ } .permissions-matrix-table { width: 100%; border-collapse: collapse; font-size: 0.9em; box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* background-color: var(--card-background); No background here, let rows/cells define it */ border: 1px solid var(--border-color); } .permissions-matrix-table th, .permissions-matrix-table td { border: 1px solid var(--border-color); padding: 10px 14px; /* Increased padding */ text-align: left; min-width: 120px; /* Minimum width for role columns */ background-color: var(--card-background); /* Default cell background */ } .permissions-matrix-table th:first-child, /* Permission column header */ .permissions-matrix-table td:first-child { /* Permission column cells */ min-width: 280px; /* Wider for permission names */ position: sticky; left: 0; /* background-color will be set by th or tr:nth-child rules */ z-index: 2; /* Above normal cells, below main header */ border-right: 2px solid var(--primary-dark); /* Emphasize sticky column */ } .permissions-matrix-table th { /* All header cells */ background-color: var(--surface-color); color: var(--primary-color); font-weight: bold; position: sticky; top: 0; z-index: 3; /* Higher z-index for header row */ border-bottom: 2px solid var(--primary-dark); /* Emphasize header row */ } /* Ensure top-left cell (Permission header) is also sticky and styled correctly */ .permissions-matrix-table th:first-child { z-index: 4 !important; /* Highest z-index for the corner */ /* Background already set by .permissions-matrix-table th */ } .permissions-matrix-table tbody tr:nth-child(even) td { /* Apply to td for sticky column */ background-color: var(--surface-color); } /* Ensure sticky first cell in even rows matches row background */ .permissions-matrix-table tbody tr:nth-child(even) td:first-child { background-color: var(--surface-color); } /* Ensure sticky first cell in odd rows matches default cell background */ .permissions-matrix-table tbody tr:nth-child(odd) td:first-child { background-color: var(--card-background); } .permissions-matrix-table tbody tr:hover td { /* Apply to all tds in hovered row */ background-color: #e0e7ef; /* A slightly different hover, less intense */ } /* Ensure sticky first cell in hovered row matches hover background */ .permissions-matrix-table tbody tr:hover td:first-child { background-color: #e0e7ef; } .permission-check { color: var(--accent-color); font-weight: bold; text-align: center; display: block; font-size: 1.2em; /* Make checkmark slightly larger */ } .permission-no { /* For empty cells, if specific styling is desired */ display: block; text-align: center; color: #cccccc; /* e.g., a light grey dash or x */ font-size: 1.2em; } /* End of Styles for Permissions Matrix Table */ </style> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> </head> <body> <div class="hero"> <div class="hero-content"> <div class="hero-meta"> <div class="hero-meta-item"> <i class="fas fa-code-branch"></i> <span>Version $version</span> </div> <div class="hero-meta-item"> <i class="fas fa-building"></i> <span>Tenant: $tenantName</span> </div> <div class="hero-meta-item"> <i class="fas fa-clock"></i> <span>Generated: $lastUpdated</span> </div> </div> <div class="hero-main"> <div class="hero-title-section"> <h1>Intune RBAC Health Check</h1> <p class="hero-subtitle">Comprehensive overview of your Intune Role-Based Access Control configuration</p> </div> <div class="hero-buttons"> <a href="https://github.com/ugurkocde/IntuneRBAC" target="_blank" class="hero-button"> <i class="fab fa-github"></i> View on GitHub </a> <a href="https://github.com/ugurkocde/IntuneRBAC/issues" target="_blank" class="hero-button"> <i class="fas fa-comment"></i> Provide Feedback </a> $navigationButtons </div> </div> </div> </div> <div class="container"> <!-- Statistics Section --> <div class='stats-container' id='rbac-statistics-section'> <h2><i class='fas fa-chart-pie'></i>RBAC Statistics</h2> <div class='stats-grid'> <div class='stat-card'> <div class='stat-card-icon'><i class='fas fa-users-cog'></i></div> <div class='stat-number'>$totalRolesCount</div> <div class='stat-label'>Total Intune Roles</div> </div> <div class='stat-card'> <div class='stat-card-icon'><i class='fas fa-user-edit'></i></div> <div class='stat-number'>$customRolesCount</div> <div class='stat-label'>Custom Roles</div> </div> <div class='stat-card'> <div class='stat-card-icon'><i class='fas fa-tags'></i></div> <div class='stat-number'>$scopeTagsCount</div> <div class='stat-label'>Scope Tags</div> </div> </div> <h2 id='security-analysis-section'><i class='fas fa-shield-alt'></i>Security Analysis</h2> <div class='stats-grid'> <div class='stat-card warning'> <div class='stat-card-icon'><i class='fas fa-ban'></i></div> <div class='stat-number'>$unusedRolesCount</div> <div class='stat-label'>Unused Roles</div> </div> <div class='stat-card info'> <div class='stat-card-icon'><i class='fas fa-layer-group'></i></div> <div class='stat-number'>$rolesWithOverlappingPermissionsCount</div> <div class='stat-label'>Roles with Overlapping Permissions</div> </div> </div> </div> "@ $htmlRolesOverviewHeader = @" <div id='roles-overview-section'> <h2><i class='fas fa-user-cog'></i> Roles Overview</h2> </div> "@ $htmlFooter = @" </div> <footer class="footer"> <div class="footer-content"> <p class="footer-text">Created by <a href="https://www.linkedin.com/in/ugurkocde/" target="_blank" class="footer-link">Ugur Koc</a></p> </div> </footer> <script> document.addEventListener('DOMContentLoaded', (event) => { // Accordion functionality var acc = document.getElementsByClassName("accordion"); for (var i = 0; i < acc.length; i++) { acc[i].addEventListener("click", function() { this.classList.toggle("active"); var panel = this.nextElementSibling; if (panel.style.maxHeight) { panel.style.maxHeight = null; panel.classList.remove("active"); } else { panel.classList.add("active"); // Show all categories by default panel.querySelectorAll('.permission-category').forEach(cat => { cat.style.display = 'block'; }); // Set active tab to "All Permissions" panel.querySelector('.permission-tab').classList.add('active'); // Set max height to allow scrolling panel.style.maxHeight = panel.scrollHeight + "px"; } }); } // Add smooth scrolling for the resource actions section document.querySelectorAll('.resource-actions').forEach(section => { section.style.scrollBehavior = 'smooth'; }); }); function showCategory(button, category) { // Update active tab document.querySelectorAll('.permission-tab').forEach(tab => tab.classList.remove('active')); button.classList.add('active'); // Show/hide categories document.querySelectorAll('.permission-category').forEach(cat => { if (category === 'all') { cat.style.display = 'block'; } else { cat.style.display = cat.dataset.category === category ? 'block' : 'none'; } }); } function filterPermissions(input) { const filter = input.value.toLowerCase(); document.querySelectorAll('.permission-item').forEach(item => { const text = item.querySelector('.permission-name').textContent.toLowerCase(); item.style.display = text.includes(filter) ? '' : 'none'; }); } </script> </body> </html> "@ function Generate-RoleRelationshipDiagramHtml { param( [System.Collections.Generic.List[object]]$Nodes, [System.Collections.Generic.List[object]]$Links ) $nodesJson = $Nodes | ConvertTo-Json -Depth 5 -Compress $linksJson = $Links | ConvertTo-Json -Depth 5 -Compress # Ensure JSON is properly escaped for embedding in a JavaScript string literal $escapedNodesJson = $nodesJson -replace '\\', '\\\\' -replace "'", "\'" -replace '"', '\"' $escapedLinksJson = $linksJson -replace '\\', '\\\\' -replace "'", "\'" -replace '"', '\"' $diagramHtml = @" <div id='role-relationship-diagram-section' class='container-section'> <h2><i class='fas fa-project-diagram'></i> Interactive Role Relationship Diagram</h2> <div class='visualization-container'> <div class='visualization-controls'> <input type="text" id="graphSearchNodes" placeholder="Search nodes..." onkeyup="searchGraphNodes()"> <button onclick="toggleGraphNodeType('role')">Toggle Roles</button> <button onclick="toggleGraphNodeType('group')">Toggle Groups</button> <button onclick="toggleGraphNodeType('user')">Toggle Users</button> <button onclick="resetGraphView()">Reset View</button> </div> <div id='roleGraphVisualization'></div> </div> </div> <style> .visualization-container { background-color: var(--surface-color); border-radius: 10px; padding: 20px; margin: 20px 0; /* Reduced top margin */ box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid var(--border-color); } #roleGraphVisualization { width: 100%; height: 700px; /* Increased height */ background-color: var(--card-background); border: 1px solid var(--border-color); border-radius: 8px; } .visualization-controls { margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } .visualization-controls button { padding: 8px 12px; /* Adjusted padding */ background-color: var(--accent-color); color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; font-size: 0.9em; } .visualization-controls button:hover { background-color: var(--accent-light); } .visualization-controls input[type="text"] { padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.9em; min-width: 200px; } </style> <script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css" type="text/css" /> <script type="text/javascript"> var graphNodes = JSON.parse('$escapedNodesJson'); var graphEdges = JSON.parse('$escapedLinksJson'); var network = null; var allNodesDataset = new vis.DataSet(graphNodes); var allEdgesDataset = new vis.DataSet(graphEdges); function drawRoleGraph() { var container = document.getElementById('roleGraphVisualization'); var data = { nodes: allNodesDataset, edges: allEdgesDataset }; var options = { nodes: { shape: 'dot', size: 18, // Slightly larger default size font: { size: 12, face: 'Segoe UI', color: '#333333' }, borderWidth: 2, shadow: { enabled: true, size: 5, x: 2, y: 2 } }, edges: { width: 2, shadow: false, smooth: { type: 'continuous', roundness: 0.2 }, arrows: { to: { enabled: true, scaleFactor: 0.7 } } }, physics: { enabled: true, solver: 'barnesHut', barnesHut: { gravitationalConstant: -15000, // Adjusted for better spread centralGravity: 0.1, // Pulls nodes slightly to center springLength: 150, // Default spring length springConstant: 0.05, damping: 0.09 }, stabilization: { iterations: 150 } // Fewer iterations for faster load }, layout: { hierarchical: false // Using physics-based layout }, groups: { role: { color: { background:'#28a745', border:'#208A38' }, shape: 'icon', icon: { face: 'FontAwesome', code: '\uf508', size: 30, color: 'white'}}, // Shield icon group: { color: { background:'#007bff', border:'#0062CC' }, shape: 'icon', icon: { face: 'FontAwesome', code: '\uf0c0', size: 30, color: 'white'}}, // Users icon user: { color: { background:'#6c757d', border:'#545B62' }, shape: 'icon', icon: { face: 'FontAwesome', code: '\uf007', size: 25, color: 'white'}} // User icon }, interaction: { hover: true, tooltipDelay: 200, navigationButtons: true, // Adds zoom buttons keyboard: true // Allows keyboard navigation } }; network = new vis.Network(container, data, options); network.on("doubleClick", function (params) { if (params.nodes.length > 0) { var nodeId = params.nodes[0]; network.focus(nodeId, { scale: 1.5, animation: true }); } }); } document.addEventListener('DOMContentLoaded', function() { if (graphNodes.length > 0) { drawRoleGraph(); } else { document.getElementById('roleGraphVisualization').innerHTML = '<p style="text-align:center;padding-top:20px;">No data available to display the relationship diagram.</p>'; } }); var originalNodesState = {}; // To store original color/size for reset allNodesDataset.getIds().forEach(function(nodeId){ var node = allNodesDataset.get(nodeId); originalNodesState[nodeId] = { color: node.color, size: node.size }; }); function searchGraphNodes() { var input = document.getElementById('graphSearchNodes'); var filter = input.value.toLowerCase(); var nodesToUpdate = []; allNodesDataset.forEach(function(node) { var labelMatch = node.label.toLowerCase().includes(filter); var titleMatch = node.title ? node.title.toLowerCase().includes(filter) : false; var isVisible = (filter === '') ? true : (labelMatch || titleMatch); var updateObj = { id: node.id }; if (filter === '') { // Reset to original updateObj.color = originalNodesState[node.id] ? originalNodesState[node.id].color : node.color; // Fallback to current if somehow not in original updateObj.size = originalNodesState[node.id] ? originalNodesState[node.id].size : node.size; } else { if (labelMatch || titleMatch) { updateObj.color = { background: '#FFD700', border: '#FFA500' }; // Highlight color updateObj.size = 25; // Emphasize size } else { // Dim non-matching nodes updateObj.color = { background: '#e0e0e0', border: '#cccccc' }; updateObj.size = 10; } } nodesToUpdate.push(updateObj); }); allNodesDataset.update(nodesToUpdate); } var hiddenNodeTypes = new Set(); function toggleGraphNodeType(type) { if (hiddenNodeTypes.has(type)) { hiddenNodeTypes.delete(type); } else { hiddenNodeTypes.add(type); } var view = new vis.DataView(allNodesDataset, { filter: function (item) { return !hiddenNodeTypes.has(item.group); } }); network.setData({nodes: view, edges: allEdgesDataset}); } function resetGraphView() { if (network) { hiddenNodeTypes.clear(); var view = new vis.DataView(allNodesDataset, { filter: function (item) { return true; } // Show all }); network.setData({nodes: view, edges: allEdgesDataset}); network.fit({animation: true}); // Fit all nodes back into view document.getElementById('graphSearchNodes').value = ''; // Clear search searchGraphNodes(); // Apply empty search to reset highlights } } </script> "@ return $diagramHtml } # Combine HTML content and save to file $permissionsMatrixHtml = Generate-PermissionsMatrixHtml $roleRelationshipDiagramHtml = Generate-RoleRelationshipDiagramHtml -Nodes $script:graphNodes -Links $script:graphLinks $htmlComplete = $htmlHeader + $htmlRolesOverviewHeader + ($htmlRolesWithScopeTags -join " ") + $permissionsMatrixHtml + $roleRelationshipDiagramHtml + $htmlFooter $htmlComplete | Out-File "rbachealthcheck.html" |