html-export.ps1
|
# Function to get assignment information for the HTML report. # Differs from the module's private Get-HtmlAssignmentInfo: aggregates all assignments # (not just the first), understands raw Graph API assignment objects (target with # @odata.type), and returns a combined Filter display string. function Get-HtmlAssignmentInfo { param ( [Parameter(Mandatory = $true)] [AllowNull()] [array]$Assignments ) if ($null -eq $Assignments -or $Assignments.Count -eq 0) { return @{ Type = "None" Target = "Not Assigned" FilterName = "None" FilterType = "None" Filter = "None" } } $types = @() $targets = @() $filterNames = @() $filterTypes = @() $filterDisplays = @() foreach ($assignment in $Assignments) { $type = "None" $target = "Not Assigned" $groupId = $null $filterId = $null $filterType = $null # Handle Graph API format (with target object) if ($assignment.target) { switch ($assignment.target.'@odata.type') { '#microsoft.graph.allLicensedUsersAssignmentTarget' { $type = "All Users" $target = "All Users" } '#microsoft.graph.allDevicesAssignmentTarget' { $type = "All Devices" $target = "All Devices" } '#microsoft.graph.groupAssignmentTarget' { $type = "Group" $groupId = $assignment.target.groupId } '#microsoft.graph.exclusionGroupAssignmentTarget' { $type = "Exclude" $groupId = $assignment.target.groupId } } $rawFilterId = $assignment.target.deviceAndAppManagementAssignmentFilterId $rawFilterType = $assignment.target.deviceAndAppManagementAssignmentFilterType if ($rawFilterType -and $rawFilterType -ne 'none' -and $rawFilterId -and $rawFilterId -ne '00000000-0000-0000-0000-000000000000') { $filterId = $rawFilterId $filterType = $rawFilterType } } # Handle standard format (with Reason and GroupId) else { $type = switch ($assignment.Reason) { "All Users" { $target = "All Users"; "All Users"; break } "All Devices" { $target = "All Devices"; "All Devices"; break } "Group Assignment" { "Group"; break } "Group Exclusion" { "Exclude"; break } "Exclude" { "Exclude"; break } default { "None" } } $groupId = $assignment.GroupId if ($assignment.FilterId -and $assignment.FilterType -and $assignment.FilterType -ne 'none') { $filterId = $assignment.FilterId $filterType = $assignment.FilterType } } # Get group name if we have a group ID if ($groupId) { $groupInfo = Get-GroupInfo -GroupId $groupId $target = $groupInfo.DisplayName } $filterName = '' $filterTypeLabel = '' if ($filterId) { if ($script:AssignmentFilterLookup -and $script:AssignmentFilterLookup.ContainsKey($filterId)) { $filterName = $script:AssignmentFilterLookup[$filterId].Name } else { $filterName = "Unknown Filter ($filterId)" } $filterTypeLabel = switch ($filterType) { 'include' { 'Include' } 'exclude' { 'Exclude' } default { $filterType } } } else { $filterName = 'None' $filterTypeLabel = 'None' } # Keep filter entries positionally aligned with the assignment targets. $filterNames += $filterName $filterTypes += $filterTypeLabel $filterDisplays += if ($filterId) { "$filterName [$filterTypeLabel]" } else { 'None' } $types += $type $targets += $target } # Determine the primary type (prioritize All Users/Devices over Group) $primaryType = if ($types -contains "All Users") { "All Users" } elseif ($types -contains "All Devices") { "All Devices" } elseif ($types -contains "Group") { "Group" } elseif ($types -contains "Exclude") { "Exclude" } else { "None" } return @{ Type = $primaryType Target = ($targets -join "; ") FilterName = $filterNames -join "; " FilterType = $filterTypes -join "; " Filter = $filterDisplays -join "; " } } # Intent template family enrichment ($script:IntentTemplateSubtypeToFamily, # Get-IntentTemplateFamilyLookup, Add-IntentTemplateFamilyInfo) comes from the # module scope, since this file is dot-sourced inside the module. function ConvertTo-IntuneCsvSafeValue { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $false)] [AllowNull()] [object]$Value ) $text = if ($null -eq $Value) { '' } else { [string]$Value } # Quoting alone does not stop spreadsheet formula injection. Prefix tenant- # controlled values that spreadsheet applications can interpret as formulas. if ($text -match '^[=+\-@\t\r]') { return "'$text" } return $text } function Export-HTMLReport { param ( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $false)] [string]$CSVReportPath, [Parameter(Mandatory = $false)] [switch]$SkipCSVReport ) # HTML template with placeholders for $tabHeaders, $tabContent, summary stats, and chart $htmlTemplate = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Intune Assignment Report</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css" rel="stylesheet"> <link href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet"> <style> :root { --bg-color: #f5f7fa; --text-color: #000; --card-bg: #fff; --table-bg: #fff; --hover-bg: #f8f9fa; --border-color: #dee2e6; } [data-theme="dark"] { --bg-color: #1a1a1a; --text-color: #fff; --card-bg: #2d2d2d; --table-bg: #2d2d2d; --hover-bg: #3d3d3d; --border-color: #404040; } body { padding: 20px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--bg-color); color: var(--text-color); transition: background-color 0.3s ease, color 0.3s ease; } .card { margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 10px; background-color: var(--card-bg); transition: transform 0.2s, background-color 0.3s ease; border-color: var(--border-color); } .card:hover { transform: translateY(-2px); } .badge-all-users { background-color: #28a745; color: white; padding: 5px 10px; border-radius: 15px; } .badge-all-devices { background-color: #17a2b8; color: white; padding: 5px 10px; border-radius: 15px; } .badge-group { background-color: #ffc107; color: black; padding: 5px 10px; border-radius: 15px; } .badge-none { background-color: #dc3545; color: white; padding: 5px 10px; border-radius: 15px; } .badge-exclude { background-color: #6c757d; color: white; padding: 5px 10px; border-radius: 15px; } .summary-card { background-color: #f8f9fa; border: none; } .table-container { margin-top: 20px; background: var(--table-bg); padding: 20px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); transition: background-color 0.3s ease; } .table { color: var(--text-color) !important; } .table thead th { color: var(--text-color) !important; } .table tbody tr:hover { background-color: var(--hover-bg) !important; } .dataTables_info, .dataTables_length, .dataTables_filter label { color: var(--text-color) !important; } .nav-tabs { margin-bottom: 20px; border-bottom: 2px solid var(--border-color); } .nav-tabs .nav-link { border: none; color: #6c757d; padding: 10px 20px; margin-right: 5px; border-radius: 5px 5px 0 0; } .nav-tabs .nav-link.active { color: #0d6efd; border-bottom: 2px solid #0d6efd; font-weight: 500; } .tab-content { padding: 20px; border: 1px solid var(--border-color); border-top: none; border-radius: 0 0 10px 10px; background-color: var(--card-bg); } .chart-container { margin: 10px 0; padding: 15px; background: var(--card-bg); border-radius: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); height: 300px; display: flex; justify-content: center; align-items: center; } .search-box { margin: 20px 0; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } .policy-table { width: 100% !important; } .policy-table thead th { background-color: #f8f9fa; font-weight: 600; } .report-header { background: linear-gradient(135deg, #0d6efd 0%, #0099ff 100%); color: white; padding: 30px; border-radius: 10px; margin-bottom: 30px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); animation: fadeIn 0.5s ease-in-out; position: relative; } .theme-toggle { position: absolute; top: 20px; right: 20px; background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; transition: transform 0.3s ease; } .theme-toggle:hover { transform: scale(1.1); } @media print { body { background-color: white !important; color: black !important; } .card, .table-container, .tab-content { background-color: white !important; color: black !important; box-shadow: none !important; } .theme-toggle, .buttons-collection { display: none !important; } .table { color: black !important; } .table thead th { color: black !important; background-color: #f8f9fa !important; } } @keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } .search-box { margin: 20px 0; padding: 20px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } .search-box input { border: 2px solid #dee2e6; transition: border-color 0.3s ease; } .search-box input:focus { border-color: #0d6efd; box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); } .report-header h1 { margin: 0; font-weight: 300; } .report-header p { margin: 10px 0 0 0; opacity: 0.9; } .summary-stat { text-align: center; padding: 20px; } .summary-stat h3 { font-size: 2rem; font-weight: 300; margin: 10px 0; color: #0d6efd; } .summary-stat p { color: #6c757d; margin: 0; } #assignmentTypeFilter { border: 2px solid #dee2e6; border-radius: 5px; padding: 8px; transition: all 0.3s ease; background-color: var(--card-bg); color: var(--text-color); } #assignmentTypeFilter:focus { border-color: #0d6efd; box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); outline: none; } .form-label { color: var(--text-color); margin-bottom: 0.5rem; font-weight: 500; } </style> </head> <body> <div class="container-fluid"> <div class="report-header"> <h1>Intune Assignment Report</h1> <p>Generated on $(Get-Date -Format "MMMM dd, yyyy HH:mm")</p> </div> <div class="row mb-4"> <div class="col-md-12"> <div class="card summary-card"> <div class="card-body"> <h5 class="card-title">Summary</h5> <div class="row" id="summary-stats"> <!-- Summary stats will be inserted here --> </div> </div> </div> <!-- Policy overview chart placeholder --> </div> </div> <div class="search-box"> <div class="row align-items-end mb-2"> <div class="col-md-6"> <div class="form-group"> <label for="groupSearch">Search by Group Name:</label> <input type="text" class="form-control" id="groupSearch" placeholder="Enter group name..."> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for="assignmentTypeFilter" class="form-label">Filter by Assignment Type:</label> <select class="form-select" id="assignmentTypeFilter"> <option value="all">All Types</option> <option value="All Users">All Users</option> <option value="All Devices">All Devices</option> <option value="Group">Group</option> <option value="None">None</option> <option value="Exclude">Exclude</option> </select> </div> </div> </div> <div class="row align-items-end"> <div class="col-md-6"> <div class="form-group"> <label for="scopeTagFilter" class="form-label">Filter by Scope Tag:</label> <select class="form-select" id="scopeTagFilter"> <option value="all">All Scope Tags</option> <!-- Scope tag options will be inserted here --> </select> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for="platformFilter" class="form-label">Filter by Platform:</label> <select class="form-select" id="platformFilter"> <option value="all">All Platforms</option> <!-- Platform options will be inserted here --> </select> </div> </div> </div> </div> <ul class="nav nav-tabs" id="assignmentTabs" role="tablist"> <!-- Tab headers will be inserted here --> </ul> <div class="tab-content" id="assignmentTabContent"> <!-- Tab content will be inserted here --> </div> </div> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.2.2/js/dataTables.buttons.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.2.2/js/buttons.bootstrap5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.2.2/js/buttons.html5.min.js"></script> <script> jQuery(document).ready(function() { // Initialize DataTables var tables = jQuery('.policy-table').DataTable({ dom: 'Blfrtip', buttons: [ 'copyHtml5', 'excelHtml5', 'csvHtml5' ], pageLength: 10, lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]], ordering: false, columnDefs: [ { targets: '_all', orderable: false } ] }); // Helper: find column index by header text function findColumnIndex(dataTable, headerText) { return dataTable.columns().header().toArray().findIndex(function(h) { return h.textContent.trim() === headerText; }); } // Assignment Type Filter jQuery('#assignmentTypeFilter').on('change', function() { const filterValue = jQuery(this).val(); jQuery('.policy-table').each(function() { const dataTable = jQuery(this).DataTable(); var colIdx = findColumnIndex(dataTable, 'Assignment Type'); if (colIdx >= 0) { if (filterValue === 'all') { dataTable.column(colIdx).search('').draw(); } else { dataTable.column(colIdx).search(filterValue, false, false).draw(); } } }); }); // Scope Tag Filter jQuery('#scopeTagFilter').on('change', function() { const filterValue = jQuery(this).val(); jQuery('.policy-table').each(function() { const dataTable = jQuery(this).DataTable(); var colIdx = findColumnIndex(dataTable, 'Scope Tags'); if (colIdx >= 0) { if (filterValue === 'all') { dataTable.column(colIdx).search('').draw(); } else { var escaped = filterValue.replace(/[.*+?^`${}()|[\]\\]/g, '\\`$&'); dataTable.column(colIdx).search('(?:^|,\\s*)' + escaped + '(?:\\s*,|`$)', true, false).draw(); } } }); }); // Platform Filter jQuery('#platformFilter').on('change', function() { const filterValue = jQuery(this).val(); jQuery('.policy-table').each(function() { const dataTable = jQuery(this).DataTable(); var colIdx = findColumnIndex(dataTable, 'Platform'); if (colIdx >= 0) { if (filterValue === 'all') { dataTable.column(colIdx).search('').draw(); } else { var escaped = filterValue.replace(/[.*+?^`${}()|[\]\\]/g, '\\`$&'); dataTable.column(colIdx).search('^' + escaped + '`$', true, false).draw(); } } }); }); jQuery('#groupSearch').on('keyup', function() { const searchTerm = this.value.toLowerCase(); jQuery('.policy-table').each(function() { jQuery(this).DataTable().search(searchTerm).draw(); }); }); // Show the first tab by default const firstTab = document.querySelector('.nav-tabs .nav-link'); const firstPane = document.querySelector('.tab-pane'); if (firstTab) firstTab.classList.add('active'); if (firstPane) firstPane.classList.add('show', 'active'); }); </script> </body> </html> "@ # Initialize collections $policies = @{ DeviceConfigs = @() ImportedAdministrativeTemplates = @() SettingsCatalog = @() CompliancePolicies = @() AppProtectionPolicies = @() PlatformScripts = @() HealthScripts = @() RequiredApps = @() AvailableApps = @() UninstallApps = @() DeploymentProfiles = @() ESPProfiles = @() AntivirusProfiles = @() DiskEncryptionProfiles = @() FirewallProfiles = @() EndpointDetectionProfiles = @() AttackSurfaceProfiles = @() AccountProtectionProfiles = @() CloudPCProvisioningPolicies = @() CloudPCUserSettings = @() WindowsFeatureUpdates = @() WindowsQualityUpdates = @() WindowsDriverUpdates = @() WindowsQualityUpdatePolicies = @() } # Fetch all policies Write-Host "Fetching Device Configurations..." -ForegroundColor Yellow $deviceConfigs = Get-IntuneEntities -EntityType "deviceConfigurations" foreach ($config in $deviceConfigs) { $assignments = Get-IntuneAssignments -EntityType "deviceConfigurations" -EntityId $config.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.DeviceConfigs += @{ Name = $config.displayName ID = $config.id Type = "Device Configuration" Platform = Get-PolicyPlatform -Policy $config ScopeTags = Get-ScopeTagNames -ScopeTagIds $config.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } Write-Host "Fetching Imported Administrative Templates..." -ForegroundColor Yellow $importedTemplates = Get-IntuneEntities -EntityType "groupPolicyConfigurations" | Where-Object { Test-ImportedAdministrativeTemplate -Policy $_ } foreach ($policy in $importedTemplates) { $assignments = Get-IntuneAssignments -EntityType "groupPolicyConfigurations" -EntityId $policy.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.ImportedAdministrativeTemplates += @{ Name = $policy.displayName ID = $policy.id Type = "Imported Administrative Template" Platform = "Windows" ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } Write-Host "Fetching Settings Catalog Policies..." -ForegroundColor Yellow $settingsCatalog = Get-IntuneEntities -EntityType "configurationPolicies" foreach ($policy in $settingsCatalog) { # Exclude Endpoint Security policies from this generic Settings Catalog fetch if ($policy.templateReference -and $policy.templateReference.templateFamily -like "endpointSecurity*") { continue } $assignments = Get-IntuneAssignments -EntityType "configurationPolicies" -EntityId $policy.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.SettingsCatalog += @{ Name = if (-not [string]::IsNullOrWhiteSpace($policy.displayName)) { $policy.displayName } else { $policy.name } ID = $policy.id Type = "Settings Catalog" Platform = Get-PolicyPlatform -Policy $policy ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } Write-Host "Fetching Compliance Policies..." -ForegroundColor Yellow $compliancePolicies = Get-IntuneEntities -EntityType "deviceCompliancePolicies" foreach ($policy in $compliancePolicies) { $assignments = Get-IntuneAssignments -EntityType "deviceCompliancePolicies" -EntityId $policy.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.CompliancePolicies += @{ Name = $policy.displayName ID = $policy.id Type = "Compliance Policy" Platform = Get-PolicyPlatform -Policy $policy ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } Write-Host "Fetching App Protection Policies..." -ForegroundColor Yellow $appProtectionPolicies = Get-IntuneEntities -EntityType "deviceAppManagement/managedAppPolicies" foreach ($policy in $appProtectionPolicies) { $policyType = $policy.'@odata.type' $assignmentsUri = Get-AppProtectionAssignmentUri -Policy $policy if ($assignmentsUri) { try { $assignmentResponse = Invoke-IACGraphRequest -Uri $assignmentsUri -Method Get # Pass the raw .value to Get-HtmlAssignmentInfo as it expects an array of assignment objects $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignmentResponse.value $policies.AppProtectionPolicies += @{ Name = $policy.displayName ID = $policy.id Type = "App Protection Policy ($($policyType.Split('.')[-1].Replace('ManagedAppProtection','')))" Platform = Get-PolicyPlatform -Policy $policy ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } catch { Write-Host "Error fetching assignments for App Protection policy $($policy.displayName): $($_.Exception.Message)" -ForegroundColor Red } } } # Get Platform Scripts Write-Host "Fetching Platform Scripts..." -ForegroundColor Yellow $platformScripts = Get-IntuneEntities -EntityType "deviceManagementScripts" foreach ($script in $platformScripts) { $assignments = Get-IntuneAssignments -EntityType "deviceManagementScripts" -EntityId $script.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.PlatformScripts += @{ Name = $script.displayName ID = $script.id Type = "PowerShell Script" Platform = "Windows" ScopeTags = Get-ScopeTagNames -ScopeTagIds $script.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } # Get Proactive Remediation Scripts Write-Host "Fetching Proactive Remediation Scripts..." -ForegroundColor Yellow $healthScripts = Get-IntuneEntities -EntityType "deviceHealthScripts" foreach ($script in $healthScripts) { $assignments = Get-IntuneAssignments -EntityType "deviceHealthScripts" -EntityId $script.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.HealthScripts += @{ Name = $script.displayName ID = $script.id Type = "Proactive Remediation Script" Platform = "Windows" ScopeTags = Get-ScopeTagNames -ScopeTagIds $script.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } # Get Autopilot Deployment Profiles Write-Host "Fetching Autopilot Deployment Profiles..." -ForegroundColor Yellow $autoProfiles = Get-IntuneEntities -EntityType "windowsAutopilotDeploymentProfiles" foreach ($autoProfile in $autoProfiles) { $assignments = Get-IntuneAssignments -EntityType "windowsAutopilotDeploymentProfiles" -EntityId $autoProfile.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.DeploymentProfiles += @{ Name = $autoProfile.displayName ID = $autoProfile.id Type = "Autopilot Deployment Profile" Platform = "Windows" ScopeTags = Get-ScopeTagNames -ScopeTagIds $autoProfile.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } # Get Enrollment Status Page Profiles Write-Host "Fetching Enrollment Status Page Profiles..." -ForegroundColor Yellow $enrollmentConfigs = Get-IntuneEntities -EntityType "deviceEnrollmentConfigurations" $espProfiles = $enrollmentConfigs | Where-Object { $_.'@odata.type' -match 'EnrollmentCompletionPageConfiguration' } foreach ($esp in $espProfiles) { $assignments = Get-IntuneAssignments -EntityType "deviceEnrollmentConfigurations" -EntityId $esp.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies.ESPProfiles += @{ Name = $esp.displayName ID = $esp.id Type = "Enrollment Status Page" Platform = "Windows" ScopeTags = Get-ScopeTagNames -ScopeTagIds $esp.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } # Get Windows 365 Cloud PC Provisioning Policies Write-Host "Fetching Windows 365 Cloud PC Provisioning Policies..." -ForegroundColor Yellow try { $cloudPCProvisioningPolicies = Get-IntuneEntities -EntityType "virtualEndpoint/provisioningPolicies" foreach ($policy in $cloudPCProvisioningPolicies) { $rawAssignments = Get-IntuneAssignments -EntityType "virtualEndpoint/provisioningPolicies" -EntityId $policy.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $rawAssignments $policies.CloudPCProvisioningPolicies += @{ Name = if (-not [string]::IsNullOrWhiteSpace($policy.displayName)) { $policy.displayName } else { $policy.name } ID = $policy.id Type = "Windows 365 Cloud PC Provisioning Policy" Platform = "Windows" ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } } catch { Write-Warning "Unable to fetch Windows 365 Cloud PC Provisioning Policies: $($_.Exception.Message)" } # Get Windows 365 Cloud PC User Settings Write-Host "Fetching Windows 365 Cloud PC User Settings..." -ForegroundColor Yellow try { $cloudPCUserSettings = Get-IntuneEntities -EntityType "virtualEndpoint/userSettings" foreach ($setting in $cloudPCUserSettings) { $rawAssignments = Get-IntuneAssignments -EntityType "virtualEndpoint/userSettings" -EntityId $setting.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $rawAssignments $policies.CloudPCUserSettings += @{ Name = if (-not [string]::IsNullOrWhiteSpace($setting.displayName)) { $setting.displayName } else { $setting.name } ID = $setting.id Type = "Windows 365 Cloud PC User Setting" Platform = "Windows" ScopeTags = Get-ScopeTagNames -ScopeTagIds $setting.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } } catch { Write-Warning "Unable to fetch Windows 365 Cloud PC User Settings: $($_.Exception.Message)" } # Windows Update for Business reports. Each workload is optional so a tenant # that lacks one feature cannot hide unrelated report categories. foreach ($updateSpec in @( @{ EntityType = 'windowsFeatureUpdateProfiles'; Key = 'WindowsFeatureUpdates'; Type = 'Windows Feature Update Profile'; Label = 'Windows Feature Update Profiles' } @{ EntityType = 'windowsQualityUpdateProfiles'; Key = 'WindowsQualityUpdates'; Type = 'Windows Quality Update Profile'; Label = 'Windows Quality Update Profiles' } @{ EntityType = 'windowsDriverUpdateProfiles'; Key = 'WindowsDriverUpdates'; Type = 'Windows Driver Update Profile'; Label = 'Windows Driver Update Profiles' } @{ EntityType = 'windowsQualityUpdatePolicies'; Key = 'WindowsQualityUpdatePolicies'; Type = 'Windows Quality Update Policy'; Label = 'Windows Quality Update Policies' } )) { Write-Host "Fetching $($updateSpec.Label)..." -ForegroundColor Yellow foreach ($policy in @(Get-IntuneEntities -EntityType $updateSpec.EntityType -Quiet)) { $assignments = Get-IntuneAssignments -EntityType $updateSpec.EntityType -EntityId $policy.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignments $policies[$updateSpec.Key] += @{ Name = if ([string]::IsNullOrWhiteSpace($policy.displayName)) { $policy.name } else { $policy.displayName } ID = $policy.id Type = $updateSpec.Type Platform = 'Windows' ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } } # Endpoint Security Policies Fetching $endpointSecurityCategories = @( @{ Name = "Antivirus"; Key = "AntivirusProfiles"; TemplateFamily = "endpointSecurityAntivirus"; UserFriendlyType = "Antivirus Profile" }, @{ Name = "Disk Encryption"; Key = "DiskEncryptionProfiles"; TemplateFamily = "endpointSecurityDiskEncryption"; UserFriendlyType = "Disk Encryption Profile" }, @{ Name = "Firewall"; Key = "FirewallProfiles"; TemplateFamily = "endpointSecurityFirewall"; UserFriendlyType = "Firewall Profile" }, @{ Name = "Endpoint Detection and Response"; Key = "EndpointDetectionProfiles"; TemplateFamily = "endpointSecurityEndpointDetectionAndResponse"; UserFriendlyType = "EDR Profile" }, @{ Name = "Attack Surface Reduction"; Key = "AttackSurfaceProfiles"; TemplateFamily = "endpointSecurityAttackSurfaceReduction"; UserFriendlyType = "ASR Profile" }, @{ Name = "Account Protection"; Key = "AccountProtectionProfiles"; TemplateFamily = "endpointSecurityAccountProtection"; UserFriendlyType = "Account Protection Profile" } ) foreach ($esCategory in $endpointSecurityCategories) { Write-Host "Fetching Endpoint Security - $($esCategory.Name) Policies..." -ForegroundColor Yellow $processedIds = [System.Collections.Generic.HashSet[string]]::new() # 1. Check configurationPolicies (Settings Catalog) $allConfigPolicies = Get-IntuneEntities -EntityType "configurationPolicies" $configPolicies = $allConfigPolicies | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq $esCategory.TemplateFamily } if ($configPolicies) { foreach ($policy in $configPolicies) { if ($processedIds.Add($policy.id)) { $rawAssignments = Get-IntuneAssignments -EntityType "configurationPolicies" -EntityId $policy.id $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $rawAssignments $policies[$esCategory.Key] += @{ Name = if (-not [string]::IsNullOrWhiteSpace($policy.displayName)) { $policy.displayName } else { $policy.name } ID = $policy.id Type = $esCategory.UserFriendlyType Platform = Get-PolicyPlatform -Policy $policy ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } } } # 2. Check deviceManagement/intents (Templates) $allIntentPolicies = Get-IntuneEntities -EntityType "deviceManagement/intents" Add-IntentTemplateFamilyInfo -IntentPolicies $allIntentPolicies $intentPolicies = $allIntentPolicies | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq $esCategory.TemplateFamily } if ($intentPolicies) { foreach ($policy in $intentPolicies) { if ($processedIds.Add($policy.id)) { try { $assignmentsResponse = Invoke-IACGraphRequest -Uri "$script:GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $assignmentsResponse.value # This expects an array $policies[$esCategory.Key] += @{ Name = if (-not [string]::IsNullOrWhiteSpace($policy.displayName)) { $policy.displayName } else { $policy.name } ID = $policy.id Type = $esCategory.UserFriendlyType Platform = Get-PolicyPlatform -Policy $policy ScopeTags = Get-ScopeTagNames -ScopeTagIds $policy.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } } catch { Write-Host "Error fetching assignments for $($esCategory.Name) intent $($policy.displayName): $($_.Exception.Message)" -ForegroundColor Red } } } } } # Get Apps Write-Host "Fetching Applications..." -ForegroundColor Yellow $appUri = "$script:GraphEndpoint/beta/deviceAppManagement/mobileApps?`$filter=isAssigned eq true&`$select=id,displayName,roleScopeTagIds" $allApps = [System.Collections.Generic.List[object]]::new() try { $pagedApps = @((Invoke-IACGraphRequest -Uri $appUri -Method Get).value) if ($pagedApps.Count -gt 0) { $allApps.AddRange([object[]]$pagedApps) } } catch { Write-Host "Error fetching applications: $($_.Exception.Message)" -ForegroundColor Red Write-Error -Message "Applications fetch failed: $($_.Exception.Message)" } foreach ($app in $allApps) { $appId = $app.id $assignmentsUri = "$script:GraphEndpoint/beta/deviceAppManagement/mobileApps('$appId')/assignments" try { $assignmentResponse = Invoke-IACGraphRequest -Uri $assignmentsUri -Method Get } catch { Write-Host "Error fetching assignments for app $($app.displayName): $($_.Exception.Message)" -ForegroundColor Red Write-Error -Message "Assignments fetch failed for '$($app.displayName)': $($_.Exception.Message)" continue } foreach ($assignment in $assignmentResponse.value) { # Get-HtmlAssignmentInfo expects an array of assignment objects. # Here, $assignment is a single assignment object from the loop. # We need to wrap it in an array for Get-HtmlAssignmentInfo. $currentAssignmentArray = @($assignment) # Ensure it's an array $assignmentInfo = Get-HtmlAssignmentInfo -Assignments $currentAssignmentArray $appInfo = @{ Name = $app.displayName ID = $app.id Type = "Application" Platform = Get-PolicyPlatform -Policy $app ScopeTags = Get-ScopeTagNames -ScopeTagIds $app.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup AssignmentType = $assignmentInfo.Type AssignedTo = $assignmentInfo.Target Filter = $assignmentInfo.Filter } switch ($assignment.intent) { "required" { $policies.RequiredApps += $appInfo } "available" { $policies.AvailableApps += $appInfo } "uninstall" { $policies.UninstallApps += $appInfo } } } } # Collect unique scope tags across all policies $allScopeTags = [System.Collections.Generic.HashSet[string]]::new() foreach ($catKey in $policies.Keys) { foreach ($p in $policies[$catKey]) { if ($p.ScopeTags) { foreach ($tag in ($p.ScopeTags -split ',')) { $trimmed = $tag.Trim() if ($trimmed) { [void]$allScopeTags.Add($trimmed) } } } } } $scopeTagOptions = ($allScopeTags | Sort-Object | ForEach-Object { $escaped = $_ -replace '&', '&' -replace "'", ''' -replace '<', '<' -replace '>', '>' "<option value='$escaped'>$escaped</option>" }) -join "`n " # Collect unique platforms across all policies $allPlatforms = [System.Collections.Generic.HashSet[string]]::new() foreach ($catKey in $policies.Keys) { foreach ($p in $policies[$catKey]) { if ($p.Platform) { $trimmed = $p.Platform.Trim() if ($trimmed) { [void]$allPlatforms.Add($trimmed) } } } } $platformOptions = ($allPlatforms | Sort-Object | ForEach-Object { $escaped = $_ -replace '&', '&' -replace "'", ''' -replace '<', '<' -replace '>', '>' "<option value='$escaped'>$escaped</option>" }) -join "`n " # Generate summary statistics $summaryStats = @{ TotalPolicies = 0 AllUsers = 0 AllDevices = 0 GroupAssigned = 0 Unassigned = 0 } $categories = @( @{ Key = 'all'; Name = 'All Policies & Apps' }, @{ Key = 'DeviceConfigs'; Name = 'Device Configurations' }, @{ Key = 'ImportedAdministrativeTemplates'; Name = 'Imported Administrative Templates' }, @{ Key = 'SettingsCatalog'; Name = 'Settings Catalog' }, @{ Key = 'CompliancePolicies'; Name = 'Compliance Policies' }, @{ Key = 'AppProtectionPolicies'; Name = 'App Protection Policies' }, @{ Key = 'RequiredApps'; Name = 'Required Applications' }, @{ Key = 'AvailableApps'; Name = 'Available Applications' }, @{ Key = 'UninstallApps'; Name = 'Uninstall Applications' }, @{ Key = 'PlatformScripts'; Name = 'Platform Scripts' }, @{ Key = 'HealthScripts'; Name = 'Proactive Remediation Scripts' }, @{ Key = 'DeploymentProfiles'; Name = 'Autopilot Deployment Profiles' }, @{ Key = 'ESPProfiles'; Name = 'Enrollment Status Page Profiles' }, @{ Key = 'CloudPCProvisioningPolicies'; Name = 'Windows 365 Cloud PC Provisioning Policies' }, @{ Key = 'CloudPCUserSettings'; Name = 'Windows 365 Cloud PC User Settings' }, @{ Key = 'WindowsFeatureUpdates'; Name = 'Windows Feature Update Profiles' }, @{ Key = 'WindowsQualityUpdates'; Name = 'Windows Quality Update Profiles' }, @{ Key = 'WindowsDriverUpdates'; Name = 'Windows Driver Update Profiles' }, @{ Key = 'WindowsQualityUpdatePolicies'; Name = 'Windows Quality Update Policies' }, @{ Key = 'AntivirusProfiles'; Name = 'Endpoint Security - Antivirus' }, @{ Key = 'DiskEncryptionProfiles'; Name = 'Endpoint Security - Disk Encryption' }, @{ Key = 'FirewallProfiles'; Name = 'Endpoint Security - Firewall' }, @{ Key = 'EndpointDetectionProfiles'; Name = 'Endpoint Security - EDR' }, @{ Key = 'AttackSurfaceProfiles'; Name = 'Endpoint Security - ASR' }, @{ Key = 'AccountProtectionProfiles'; Name = 'Endpoint Security - Account Protection' } ) # Recalculate summary stats for all defined categories in $policies foreach ($category in $categories | Where-Object { $_.Key -ne 'all' }) { if ($policies.ContainsKey($category.Key)) { $items = $policies[$category.Key] if ($null -ne $items) { $summaryStats.TotalPolicies += $items.Count $summaryStats.AllUsers += ($items | Where-Object { $_.AssignmentType -eq "All Users" }).Count $summaryStats.AllDevices += ($items | Where-Object { $_.AssignmentType -eq "All Devices" }).Count $summaryStats.GroupAssigned += ($items | Where-Object { $_.AssignmentType -eq "Group" }).Count $summaryStats.Unassigned += ($items | Where-Object { $_.AssignmentType -eq "None" }).Count } } } # Build dynamic tab headers and tab content $tabHeaders = "" $tabContent = "" foreach ($category in $categories) { $isActive = ($category -eq $categories[0]) $categoryId = $category.Key.ToLower() $tabHeaders += @" <li class='nav-item' role='presentation'> <button class='nav-link$(if($isActive -and $category.Key -ne 'all'){ ' active' } else { '' })' id='$categoryId-tab' data-bs-toggle='tab' data-bs-target='#$categoryId' type='button' role='tab' aria-controls='$categoryId' aria-selected='$(if($isActive -and $category.Key -ne 'all'){ 'true' } else { 'false' })'> $($category.Name) </button> </li> "@ if ($category.Key -eq 'all') { $allTableRows = foreach ($cat in $categories | Where-Object { $_.Key -ne 'all' }) { if ($policies.ContainsKey($cat.Key)) { $categoryPolicies = $policies[$cat.Key] if ($categoryPolicies) { foreach ($p in $categoryPolicies) { $badgeClass = switch ($p.AssignmentType) { 'All Users' { 'badge-all-users' } 'All Devices' { 'badge-all-devices' } 'Group' { 'badge-group' } 'Exclude' { 'badge-exclude' } default { 'badge-none' } } "<tr> <td>$([System.Net.WebUtility]::HtmlEncode($p.Name))</td> <td>$([System.Net.WebUtility]::HtmlEncode($p.Platform))</td> <td>$([System.Net.WebUtility]::HtmlEncode($p.ScopeTags))</td> <td><span class='badge $badgeClass'>$([System.Net.WebUtility]::HtmlEncode($p.AssignmentType))</span></td> <td>$([System.Net.WebUtility]::HtmlEncode($p.AssignedTo))</td> <td>$([System.Net.WebUtility]::HtmlEncode($p.Filter))</td> </tr>" } } } } $tabContent += @" <div class='tab-pane fade$(if($isActive){ ' show active' } else { '' })' id='$categoryId' role='tabpanel' aria-labelledby='$categoryId-tab'> <div class='table-container'> <table class='table table-striped policy-table'> <thead> <tr> <th>Name</th> <th>Platform</th> <th>Scope Tags</th> <th>Assignment Type</th> <th>Assigned To</th> <th>Filter</th> </tr> </thead> <tbody> $($allTableRows -join "`n") </tbody> </table> </div> </div> "@ } else { $tableRows = "" # Initialize to empty string if ($policies.ContainsKey($category.Key)) { $currentCategoryPolicies = $policies[$category.Key] if ($currentCategoryPolicies) { $tableRows = foreach ($p in $currentCategoryPolicies) { $badgeClass = switch ($p.AssignmentType) { 'All Users' { 'badge-all-users' } 'All Devices' { 'badge-all-devices' } 'Group' { 'badge-group' } 'Exclude' { 'badge-exclude' } default { 'badge-none' } } "<tr> <td>$([System.Net.WebUtility]::HtmlEncode($p.Name))</td> <td>$([System.Net.WebUtility]::HtmlEncode($p.Platform))</td> <td>$([System.Net.WebUtility]::HtmlEncode($p.ScopeTags))</td> <td><span class='badge $badgeClass'>$([System.Net.WebUtility]::HtmlEncode($p.AssignmentType))</span></td> <td>$([System.Net.WebUtility]::HtmlEncode($p.AssignedTo))</td> <td>$([System.Net.WebUtility]::HtmlEncode($p.Filter))</td> </tr>" } } } $categoryHeaders = "<th>Name</th> <th>Platform</th> <th>Scope Tags</th> <th>Assignment Type</th> <th>Assigned To</th> <th>Filter</th>" $tabContent += @" <div class='tab-pane fade$(if($isActive -and $category.Key -ne 'all'){ ' show active' } else { '' })' id='$categoryId' role='tabpanel' aria-labelledby='$categoryId-tab'> <div class='table-container'> <table class='table table-striped policy-table'> <thead> <tr> $categoryHeaders </tr> </thead> <tbody> $($tableRows -join "`n") </tbody> </table> </div> </div> "@ } } # Summary cards $summaryCards = @" <div class='col'> <div class='card text-center summary-card'> <div class='card-body'> <i class='fas fa-layer-group mb-3' style='font-size:2rem;color:#0d6efd;'></i> <h5 class='card-title'>Total Policies</h5> <h3 class='card-text'>$($summaryStats.TotalPolicies)</h3> <p class='text-muted small'>Total configured policies</p> </div> </div> </div> <div class='col'> <div class='card text-center summary-card'> <div class='card-body'> <i class='fas fa-users mb-3' style='font-size:2rem;color:#28a745;'></i> <h5 class='card-title'>All Users</h5> <h3 class='card-text'>$($summaryStats.AllUsers)</h3> <p class='text-muted small'>Assigned to all users</p> </div> </div> </div> <div class='col'> <div class='card text-center summary-card'> <div class='card-body'> <i class='fas fa-laptop mb-3' style='font-size:2rem;color:#17a2b8;'></i> <h5 class='card-title'>All Devices</h5> <h3 class='card-text'>$($summaryStats.AllDevices)</h3> <p class='text-muted small'>Assigned to all devices</p> </div> </div> </div> <div class='col'> <div class='card text-center summary-card'> <div class='card-body'> <i class='fas fa-object-group mb-3' style='font-size:2rem;color:#ffc107;'></i> <h5 class='card-title'>Group Assigned</h5> <h3 class='card-text'>$($summaryStats.GroupAssigned)</h3> <p class='text-muted small'>Assigned to specific groups</p> </div> </div> </div> <div class='col'> <div class='card text-center summary-card'> <div class='card-body'> <i class='fas fa-exclamation-triangle mb-3' style='font-size:2rem;color:#dc3545;'></i> <h5 class='card-title'>Unassigned</h5> <h3 class='card-text'>$($summaryStats.Unassigned)</h3> <p class='text-muted small'>Not assigned to any target</p> </div> </div> </div> "@ # Insert chart container + Chart.js script $chartBlock = @" <div class='row'> <div class='col-md-6'> <div class='chart-container'> <canvas id='policyDistributionChart'></canvas> </div> </div> <div class='col-md-6'> <div class='chart-container'> <canvas id='policyTypesChart'></canvas> </div> </div> </div> <script src='https://cdn.jsdelivr.net/npm/chart.js'></script> <script> // Policy Distribution Pie Chart var ctx1 = document.getElementById('policyDistributionChart').getContext('2d'); var policyDistributionChart = new Chart(ctx1, { type: 'pie', data: { labels: ['All Users', 'All Devices', 'Group Assigned', 'Unassigned'], datasets: [{ data: [$($summaryStats.AllUsers), $($summaryStats.AllDevices), $($summaryStats.GroupAssigned), $($summaryStats.Unassigned)], backgroundColor: ['#28a745', '#17a2b8', '#ffc107', '#dc3545'], hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'bottom', labels: { font: { size: 10 } } }, title: { display: true, text: 'Policy Assignment Distribution', font: { size: 14 } } } } }); // Policy Types Bar Chart var ctx2 = document.getElementById('policyTypesChart').getContext('2d'); var policyTypesChart = new Chart(ctx2, { type: 'bar', data: { labels: ['Device Configs', 'Imported Admin Templates', 'Settings Catalog', 'Compliance', 'App Protection', 'Autopilot Profiles', 'ESP Profiles', 'Windows 365 Provisioning', 'Windows 365 User Settings', 'Feature Updates', 'Quality Profiles', 'Driver Updates', 'Quality Policies', 'Scripts', 'Antivirus', 'Disk Encryption', 'Firewall', 'EDR', 'ASR', 'Account Protection'], datasets: [{ label: 'Number of Policies', data: [ $($policies.DeviceConfigs.Count), $($policies.ImportedAdministrativeTemplates.Count), $($policies.SettingsCatalog.Count), $($policies.CompliancePolicies.Count), $($policies.AppProtectionPolicies.Count), $($policies.DeploymentProfiles.Count), $($policies.ESPProfiles.Count), $($policies.CloudPCProvisioningPolicies.Count), $($policies.CloudPCUserSettings.Count), $($policies.WindowsFeatureUpdates.Count), $($policies.WindowsQualityUpdates.Count), $($policies.WindowsDriverUpdates.Count), $($policies.WindowsQualityUpdatePolicies.Count), ($($policies.PlatformScripts.Count) + $($policies.HealthScripts.Count)), $($policies.AntivirusProfiles.Count), $($policies.DiskEncryptionProfiles.Count), $($policies.FirewallProfiles.Count), $($policies.EndpointDetectionProfiles.Count), $($policies.AttackSurfaceProfiles.Count), $($policies.AccountProtectionProfiles.Count) ], backgroundColor: [ '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b', '#6f42c1', '#20c997', '#17a2b8', '#fd7e14', '#0d6efd', '#198754', '#ffc107', '#dc3545', '#858796', '#5a5c69', '#f8f9fc', '#dddfeb', '#d1d3e2', '#b4b6c2', '#6610f2' ] }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false }, title: { display: true, text: 'Policy Types Distribution', font: { size: 14 } } }, scales: { y: { beginAtZero: true, ticks: { font: { size: 10 } } }, x: { ticks: { font: { size: 10 } } } } } }); </script> "@ # Final HTML $htmlContent = $htmlTemplate ` -replace '<!-- Tab headers will be inserted here -->', $tabHeaders ` -replace '<!-- Tab content will be inserted here -->', $tabContent ` -replace '<!-- Summary stats will be inserted here -->', $summaryCards ` -replace '<!-- Policy overview chart placeholder -->', $chartBlock ` -replace '<!-- Scope tag options will be inserted here -->', $scopeTagOptions ` -replace '<!-- Platform options will be inserted here -->', $platformOptions # Output file $htmlContent | Out-File -FilePath $FilePath -Encoding UTF8 Write-Host "HTML report exported to: $FilePath" -ForegroundColor Green if ($SkipCSVReport) { return } # Export the same report data as a flat CSV for Log Analytics, workbooks, # and other centralized processing. The aggregate "all" tab is skipped so # every policy/app appears exactly once under its source category. $resolvedCsvPath = $CSVReportPath try { if ([string]::IsNullOrWhiteSpace($CSVReportPath)) { $resolvedCsvPath = [System.IO.Path]::ChangeExtension($FilePath, '.csv') } else { # Resolve relative paths against PowerShell's current provider # location. The .NET process directory does not follow Set-Location. $requestedCsvPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($CSVReportPath) $csvFileName = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetFileName($FilePath), '.csv') if (Test-Path -Path $requestedCsvPath -PathType Container) { $resolvedCsvPath = Join-Path $requestedCsvPath $csvFileName } elseif ($requestedCsvPath -match '\.csv$') { $resolvedCsvPath = $requestedCsvPath } else { if (Test-Path -Path $requestedCsvPath -PathType Leaf) { throw "CSV report path '$requestedCsvPath' is an existing file without a .csv extension." } if ([System.IO.Path]::GetExtension($requestedCsvPath)) { throw "CSV report path '$requestedCsvPath' must be a .csv file or an existing/new directory without a file extension." } New-Item -ItemType Directory -Path $requestedCsvPath -Force -ErrorAction Stop | Out-Null $resolvedCsvPath = Join-Path $requestedCsvPath $csvFileName } } $csvParent = Split-Path -Path $resolvedCsvPath -Parent if ($csvParent -and -not (Test-Path -Path $csvParent -PathType Container)) { New-Item -ItemType Directory -Path $csvParent -Force -ErrorAction Stop | Out-Null } if (Test-Path -Path $resolvedCsvPath -PathType Leaf) { Write-Host "Replacing existing CSV report: $resolvedCsvPath" -ForegroundColor Yellow } $csvRows = [System.Collections.Generic.List[object]]::new() foreach ($category in $categories | Where-Object { $_.Key -ne 'all' }) { if (-not $policies.ContainsKey($category.Key)) { continue } foreach ($policy in @($policies[$category.Key])) { $csvRows.Add([PSCustomObject][ordered]@{ Category = ConvertTo-IntuneCsvSafeValue $category.Name Name = ConvertTo-IntuneCsvSafeValue $policy.Name ID = ConvertTo-IntuneCsvSafeValue $policy.ID Type = ConvertTo-IntuneCsvSafeValue $policy.Type Platform = ConvertTo-IntuneCsvSafeValue $policy.Platform ScopeTags = ConvertTo-IntuneCsvSafeValue $policy.ScopeTags AssignmentType = ConvertTo-IntuneCsvSafeValue $policy.AssignmentType AssignedTo = ConvertTo-IntuneCsvSafeValue $policy.AssignedTo Filter = ConvertTo-IntuneCsvSafeValue $policy.Filter }) } } if ($csvRows.Count -gt 0) { $csvRows | Export-Csv -Path $resolvedCsvPath -NoTypeInformation -Encoding UTF8 -ErrorAction Stop } else { # Keep a stable header even when the tenant returns no supported policies. '"Category","Name","ID","Type","Platform","ScopeTags","AssignmentType","AssignedTo","Filter"' | Set-Content -Path $resolvedCsvPath -Encoding UTF8 -ErrorAction Stop } Write-Host "CSV report exported to: $resolvedCsvPath" -ForegroundColor Green } catch { Write-Warning "Failed to export CSV report to '$resolvedCsvPath': $($_.Exception.Message)" } } |