Src/Public/Invoke-AsBuiltReport.Microsoft.Intune.ps1
|
function Invoke-AsBuiltReport.Microsoft.Intune { <# .SYNOPSIS PowerShell script to document the configuration of Microsoft Intune in Word/HTML/Text formats. .DESCRIPTION Documents the configuration of Microsoft Intune (Endpoint Manager) in Word/HTML/Text formats using PScribo. Covers: - Tenant Overview (identity, licensing, MDM authority) - Device Compliance Policies (per-platform, assignments, settings) - Configuration Profiles (Device Configs, Settings Catalog, Admin Templates) - App Management (App Protection Policies, published app inventory) - Enrollment Restrictions (type/limit restrictions, ESP, Autopilot) - Security Baselines (intent-based baseline profiles) - Endpoint Security (Antivirus, Disk Encryption, Firewall, EDR, ASR) - Scripts & Remediations (PowerShell, Shell, Proactive Remediations) - Managed Devices (inventory, non-compliant, stale devices) Excel Export: After report generation, an Excel workbook is automatically exported containing sheets for all major data sets for use in audits and remediation tracking. .NOTES Version: 0.1.0 Author: Pai Wei Sing Twitter: @weising17 Github: weising26 .LINK https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.Intune .PARAMETER Target Specifies the Microsoft 365 tenant domain name (e.g. contoso.onmicrosoft.com). .PARAMETER Credential Optional PSCredential. The username is used as the UPN if Options.UserPrincipalName is not set in the report config JSON. #> param ( [String[]] $Target, [PSCredential] $Credential ) Write-ReportModuleInfo -ModuleName 'Microsoft.Intune' #---------------------------------------------------------------------------------------------# # Dependency Module Version Check # #---------------------------------------------------------------------------------------------# $ModuleArray = @('AsBuiltReport.Core', 'Microsoft.Graph', 'ImportExcel') foreach ($Module in $ModuleArray) { try { $InstalledVersion = Get-Module -ListAvailable -Name $Module -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version if ($InstalledVersion) { Write-Host " - $Module module v$($InstalledVersion.ToString()) is currently installed." $PSGetAvailable = Get-Module -ListAvailable -Name PowerShellGet -ErrorAction SilentlyContinue if ($PSGetAvailable) { try { Import-Module PowerShellGet -ErrorAction SilentlyContinue $LatestVersion = Find-Module -Name $Module -Repository PSGallery -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Version if ($LatestVersion -and $InstalledVersion -lt $LatestVersion) { Write-Host " - $Module module v$($LatestVersion.ToString()) is available." -ForegroundColor Red Write-Host " - Run 'Update-Module -Name $Module -Force' to install the latest version." -ForegroundColor Red } } catch { Write-PScriboMessage -IsWarning "Unable to check latest version for ${Module}: $($_.Exception.Message)" } } else { Write-Host " - Skipping update check for $Module (PowerShellGet not available)." } } else { $Required = if ($Module -eq 'ImportExcel') { "(Optional -- required for Excel export)" } else { "" } Write-Host " - $Module module is NOT installed. $Required Run 'Install-Module -Name $Module -Force'." -ForegroundColor $(if ($Module -eq 'ImportExcel') { 'Yellow' } else { 'Red' }) } } catch { Write-PScriboMessage -IsWarning $_.Exception.Message } } #---------------------------------------------------------------------------------------------# # PRE-FLIGHT: Import Graph sub-modules BEFORE PScribo starts # #---------------------------------------------------------------------------------------------# # IMPORTANT: Graph sub-modules must be imported here, outside the PScribo document engine. # Import only the specific Graph sub-modules we need. # Importing the full Microsoft.Graph umbrella exceeds PowerShell's 4096 function limit. # NOTE: Microsoft.Graph.DeviceManagement sub-modules are intentionally NOT pre-loaded. # All Intune data is collected via Invoke-MgGraphRequest (direct REST) rather than # DeviceManagement cmdlets, so those modules are not required. Pre-loading them causes # assembly version conflict errors when Microsoft.Graph.Authentication is already loaded # by the host session (v2.24 vs v2.28 assembly mismatch). $script:RequiredGraphModules = @( 'Microsoft.Graph.Authentication' 'Microsoft.Graph.Identity.DirectoryManagement' 'Microsoft.Graph.Applications' ) Write-Host " - Pre-loading Microsoft Graph sub-modules..." -ForegroundColor Cyan foreach ($GraphModule in $script:RequiredGraphModules) { if (Get-Module -Name $GraphModule -ErrorAction SilentlyContinue) { Write-Host " - Already loaded: $GraphModule" -ForegroundColor DarkGray continue } try { Import-Module $GraphModule -Global -ErrorAction Stop Write-Host " - Loaded: $GraphModule" -ForegroundColor DarkGray } catch { Write-Warning " [!] Could not load ${GraphModule}: $($_.Exception.Message)" Write-Host " [!] DETAIL -- $($_.Exception.GetType().FullName): $($_.Exception.Message)" -ForegroundColor Red } } $script:PreFlightModuleStatus = $script:RequiredGraphModules | ForEach-Object { [pscustomobject]@{ Module = $_ Loaded = [bool](Get-Module -Name $_ -ErrorAction SilentlyContinue) } } #---------------------------------------------------------------------------------------------# # Import Report Configuration # #---------------------------------------------------------------------------------------------# $ReportConfig.Report.Name = 'Microsoft Intune As Built Report' $script:IntuneLogoPath = Join-Path $PSScriptRoot 'AsBuiltReport.Microsoft.Intune.png' if (Test-Path $script:IntuneLogoPath) { $ReportConfig.Report | Add-Member -NotePropertyName 'CoverPageImage' -NotePropertyValue $script:IntuneLogoPath -Force Write-Host " - Cover page logo set: $script:IntuneLogoPath" -ForegroundColor Cyan } $script:Report = $ReportConfig.Report $script:InfoLevel = $ReportConfig.InfoLevel $script:Options = $ReportConfig.Options $script:TextInfo = (Get-Culture).TextInfo $script:SectionTimers = [System.Collections.Generic.Dictionary[string, object]]::new() # Compliance framework toggles -- read from Options.ComplianceFrameworks $script:IncludeACSCe8 = ($script:Options.ComplianceFrameworks.ACSCe8 -eq $true) $script:IncludeCISBaseline = ($script:Options.ComplianceFrameworks.CISBaseline -eq $true) if ($script:IncludeACSCe8) { Write-Host " - Compliance framework: ACSC Essential Eight enabled" -ForegroundColor Cyan } if ($script:IncludeCISBaseline) { Write-Host " - Compliance framework: CIS Baseline enabled" -ForegroundColor Cyan } if (-not $script:IncludeACSCe8 -and -not $script:IncludeCISBaseline) { Write-Host " - Compliance frameworks: none enabled (set Options.ComplianceFrameworks in JSON to enable)" -ForegroundColor DarkGray } # Load compliance check definitions from JSON files in Src/Compliance/ # Populates $script:IntuneE8Definitions and $script:IntuneCISDefinitions Initialize-AbrIntuneComplianceFrameworks # JSON Backup -- read config from Options.JsonBackup $JsonBackupConfig = $script:Options.JsonBackup $script:JsonBackupEnabled = if ($null -ne $JsonBackupConfig -and $JsonBackupConfig.Enabled -eq $false) { $false } else { $true } if ($script:JsonBackupEnabled) { Write-Host " - JSON backup: enabled" -ForegroundColor Cyan if ($null -ne $JsonBackupConfig.IncludeSections) { $DisabledBackupSections = $JsonBackupConfig.IncludeSections.PSObject.Properties | Where-Object { $_.Value -eq $false } | ForEach-Object { $_.Name } if ($DisabledBackupSections) { Write-Host " - JSON backup sections disabled: $($DisabledBackupSections -join ', ')" -ForegroundColor Yellow } } } else { Write-Host " - JSON backup: disabled (set Options.JsonBackup.Enabled = true to enable)" -ForegroundColor DarkGray } # JSON backup data collector -- populated by each Get-Abr* function (raw Graph objects) $script:BackupData = [System.Collections.Specialized.OrderedDictionary]::new() # Excel export -- read config from Options.ExcelExport # Defaults: Enabled = true, all sheets = true (backwards-compatible if key is absent in old config files) $ExcelExportConfig = $script:Options.ExcelExport $script:ExcelEnabled = if ($null -ne $ExcelExportConfig -and $ExcelExportConfig.Enabled -eq $false) { $false } else { $true } # Get-IntuneExcelSheetEnabled and Get-IntuneBackupSectionEnabled are defined in Helpers.ps1 if ($script:ExcelEnabled) { Write-Host " - Excel export: enabled" -ForegroundColor Cyan if ($null -ne $ExcelExportConfig.Sheets) { $DisabledSheets = $ExcelExportConfig.Sheets.PSObject.Properties | Where-Object { $_.Value -eq $false } | ForEach-Object { $_.Name } if ($DisabledSheets) { Write-Host " - Excel sheets disabled: $($DisabledSheets -join ', ')" -ForegroundColor Yellow } } } else { Write-Host " - Excel export: disabled (set Options.ExcelExport.Enabled = true to enable)" -ForegroundColor Yellow } # Excel export sheet collector $script:ExcelSheets = [System.Collections.Specialized.OrderedDictionary]::new() # Compliance check accumulators -- each section appends rows with a Section column. # Consolidated into two sheets ('ACSC E8 Assessment' + 'CIS Assessment') at export time. $script:E8AllChecks = [System.Collections.ArrayList]::new() $script:CISAllChecks = [System.Collections.ArrayList]::new() # Initialise scope-confirmation flag (set to $true by Connect-IntuneSession after verifying # all required scopes are present in the token -- used by error handlers to distinguish # 403 Forbidden = missing permissions vs 403 = feature not licensed on this tenant) # Graph endpoint default -- overridden by Connect-IntuneSession based on Options.GraphEnvironment $script:GraphEndpoint = 'https://graph.microsoft.com' $script:GraphScopesFullyConfirmed = $false #---------------------------------------------------------------------------------------------# # Transcript Log Setup # #---------------------------------------------------------------------------------------------# $script:TranscriptOutputFolder = $null if ($ReportConfig.Report.OutputFolderPath -and (Test-Path $ReportConfig.Report.OutputFolderPath -ErrorAction SilentlyContinue)) { $script:TranscriptOutputFolder = $ReportConfig.Report.OutputFolderPath } elseif ($OutputFolderPath -and (Test-Path $OutputFolderPath -ErrorAction SilentlyContinue)) { $script:TranscriptOutputFolder = $OutputFolderPath } else { $script:TranscriptOutputFolder = Join-Path $env:USERPROFILE 'Documents' } $TranscriptTimestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $script:TranscriptLogPath = Join-Path $script:TranscriptOutputFolder "Intune_Transcript_${TranscriptTimestamp}.log" $script:ErrorLogPath = Join-Path $script:TranscriptOutputFolder "Intune_Errors_${TranscriptTimestamp}.log" try { Start-Transcript -Path $script:TranscriptLogPath -Append -ErrorAction Stop | Out-Null Write-Host " - Transcript log: $script:TranscriptLogPath" -ForegroundColor Cyan } catch { Write-Warning " - Could not start transcript: $($_.Exception.Message)" $script:TranscriptLogPath = $null } $script:ErrorLog = [System.Collections.Generic.List[string]]::new() function Write-IntuneError { param([string]$Section, [string]$Message) $Entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [ERROR] [$Section] $Message" $null = $script:ErrorLog.Add($Entry) Write-Host " [ERR] $Entry" -ForegroundColor Red if ($script:ErrorLogPath) { try { Add-Content -Path $script:ErrorLogPath -Value $Entry -ErrorAction SilentlyContinue } catch {} } } #---------------------------------------------------------------------------------------------# # Debug Logger Setup # #---------------------------------------------------------------------------------------------# $script:DebugLogEnabled = ($script:Options.DebugLog -eq $true) $script:DebugLogEntries = [System.Collections.Generic.List[string]]::new() function Write-AbrDebugLog { param ( [string]$Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG', 'SUCCESS')][string]$Level = 'INFO', [string]$Section = 'GENERAL' ) $Entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')] [$Level] [$Section] $Message" if ($script:DebugLogEnabled) { $null = $script:DebugLogEntries.Add($Entry) } $null = switch ($Level) { 'ERROR' { Write-Host " [DBG-ERR] $Entry" -ForegroundColor Red } 'WARN' { Write-Host " [DBG-WRN] $Entry" -ForegroundColor Yellow } 'SUCCESS' { if ($script:DebugLogEnabled) { Write-Host " [DBG] $Entry" -ForegroundColor Green } ; $null } default { if ($script:DebugLogEnabled) { Write-Host " [DBG] $Entry" -ForegroundColor DarkGray } ; $null } } } #---------------------------------------------------------------------------------------------# # Disclaimer (HealthCheck) # #---------------------------------------------------------------------------------------------# if ($Healthcheck) { Section -Style TOC -ExcludeFromTOC 'DISCLAIMER' { Paragraph 'This report was generated using the AsBuiltReport framework. Health check indicators are provided as guidance only and should be validated by a qualified administrator before remediation. Color coding: RED = Critical issue requiring immediate attention, YELLOW/AMBER = Warning requiring review.' } PageBreak } #---------------------------------------------------------------------------------------------# # Connection Section # #---------------------------------------------------------------------------------------------# foreach ($System in $Target) { Write-Host " " Write-Host "- Starting Intune report for tenant: $System" Write-TranscriptLog "Starting Intune report for tenant: $System" 'INFO' 'MAIN' Write-AbrDebugLog "Starting report for tenant: $System" 'INFO' 'MAIN' #region Resolve UPN $ResolvedUPN = $null if ($Options.UserPrincipalName -and (Test-UserPrincipalName -UserPrincipalName $Options.UserPrincipalName)) { $ResolvedUPN = $Options.UserPrincipalName } elseif ($Credential -and (Test-UserPrincipalName -UserPrincipalName $Credential.UserName)) { $ResolvedUPN = $Credential.UserName } else { throw "No valid UserPrincipalName found. Set 'Options.UserPrincipalName' in your report config JSON (e.g. admin@$System), or pass -Credential with a UPN-format username." } Write-TranscriptLog "Using UPN: $ResolvedUPN" 'INFO' 'AUTH' #endregion #region Connect try { Connect-IntuneSession -UserPrincipalName $ResolvedUPN } catch { Write-AbrDebugLog "Connection failed: $($_.Exception.Message)" 'ERROR' 'AUTH' throw "Connection failed for tenant '$System'. Error: $($_.Exception.Message)" } #endregion #region Retrieve Tenant Info Write-Host " - Retrieving tenant information..." try { $OrgResp = Invoke-MgGraphRequest -Method GET ` -Uri "$($script:GraphEndpoint)/v1.0/organization?$select=id,displayName,verifiedDomains,countryLetterCode,createdDateTime" ` -ErrorAction Stop $MgOrg = if ($OrgResp.value) { $OrgResp.value[0] } else { $OrgResp } $script:TenantId = $MgOrg.id $script:TenantDomain = ($MgOrg.verifiedDomains | Where-Object { $_.isDefault }).name if (-not $script:TenantDomain) { $script:TenantDomain = $System } $script:TenantName = if ($MgOrg.displayName) { $MgOrg.displayName } else { $script:TenantDomain } Write-Host " - Tenant: $($script:TenantName) / $($script:TenantDomain) ($($script:TenantId))" -ForegroundColor Cyan Write-TranscriptLog "Tenant identified: $($script:TenantName) / $($script:TenantDomain) ($($script:TenantId))" 'INFO' 'MAIN' } catch { $script:TenantId = $System $script:TenantDomain = $System $script:TenantName = $System Write-TranscriptLog "Unable to retrieve tenant info from Graph. Using '$System' as identifier." 'WARNING' 'MAIN' } if (-not $script:TenantName) { $script:TenantName = $System } if (-not $script:TenantDomain) { $script:TenantDomain = $System } #endregion #region Intune Licence Detection # Detects Intune Plan 1 and Plan 2 licences upfront so downstream sections # can gate themselves gracefully without making API calls that return 403. # # $script:TenantHasIntuneP1 -- gates: Scripts (PS/Shell), Device Configuration scripts # $script:TenantHasIntuneP2 -- gates: Proactive Remediations (deviceHealthScripts) # # If detection fails (e.g. no Reports.Read.All), both default to $true so sections # still attempt to run rather than silently skipping everything. $script:TenantHasIntuneP1 = $true # default open -- overridden below if SKUs detected $script:TenantHasIntuneP2 = $true # default open try { $LicResp = Invoke-MgGraphRequest -Method GET ` -Uri "$($script:GraphEndpoint)/v1.0/subscribedSkus?$select=skuPartNumber,capabilityStatus" ` -ErrorAction SilentlyContinue $SkuList = if ($LicResp.value) { $LicResp.value } else { @() } # SKUs that include Intune Plan 1 (device management, compliance, scripts) $IntuneP1Skus = @( 'INTUNE_A' # Intune standalone 'INTUNE_EDU' # Intune for Education 'EMS' # EMS E3 'EMSPREMIUM' # EMS E5 'SPE_E3' # Microsoft 365 E3 'SPE_E5' # Microsoft 365 E5 'ENTERPRISEPACK' # Office 365 E3 'ENTERPRISEPREMIUM' # Office 365 E5 'M365_BUSINESS_PREMIUM' # Microsoft 365 Business Premium 'SPB' # Microsoft 365 Business Premium (alt) 'M365EDU_A3_FACULTY' # Microsoft 365 A3 Faculty 'M365EDU_A5_FACULTY' # Microsoft 365 A5 Faculty 'DEVELOPERPACK_E5' # Microsoft 365 E5 Developer ) # SKUs that include Intune Plan 2 (Proactive Remediations, EPM, advanced features) $IntuneP2Skus = @( 'INTUNE_P2' # Intune Plan 2 standalone 'INTUNE_SUITE' # Microsoft Intune Suite 'Microsoft_Intune_Suite' # Intune Suite (alternate name) 'INTUNE_SMB' # Intune for SMB with Suite ) $ActiveSkus = $SkuList | Where-Object { $_.capabilityStatus -eq 'Enabled' } # If we can read SKUs, set flags based on what's actually licensed if ($ActiveSkus -and @($ActiveSkus).Count -gt 0) { $script:TenantHasIntuneP1 = ($ActiveSkus | Where-Object { $IntuneP1Skus -contains $_.skuPartNumber }).Count -gt 0 $script:TenantHasIntuneP2 = ($ActiveSkus | Where-Object { $IntuneP2Skus -contains $_.skuPartNumber }).Count -gt 0 # Also detect Windows 365 licence $CloudPCSkus = @('Windows_365', 'CPC_B_1C_2RAM_64GB', 'CPC_B_2C_4RAM_64GB', 'CPC_B_2C_8RAM_128GB', 'CPC_B_4C_16RAM_128GB', 'CPC_B_4C_16RAM_256GB', 'CPC_E_1C_2RAM_64GB', 'CPC_E_2C_4RAM_64GB', 'CPC_E_2C_8RAM_128GB', 'CPC_E_4C_16RAM_128GB', 'CPC_LI_1C_2RAM_64GB') $script:TenantHasCloudPC = ($ActiveSkus | Where-Object { $CloudPCSkus -contains $_.skuPartNumber }).Count -gt 0 if ($script:TenantHasIntuneP1) { Write-Host ' - Licence: Intune Plan 1 detected -- Scripts section enabled' -ForegroundColor Cyan } else { Write-Host ' - Licence: No Intune Plan 1 detected -- Scripts section will be skipped' -ForegroundColor Yellow Write-AbrDebugLog 'No Intune P1 licence detected -- Scripts section skipped' 'WARN' 'LICENCE' } if ($script:TenantHasIntuneP2) { Write-Host ' - Licence: Intune Plan 2 / Suite detected -- Proactive Remediations enabled' -ForegroundColor Cyan } else { Write-Host ' - Licence: No Intune Plan 2 / Suite -- Proactive Remediations will be skipped' -ForegroundColor Yellow Write-AbrDebugLog 'No Intune P2 licence detected -- Proactive Remediations skipped' 'WARN' 'LICENCE' } if ($script:TenantHasCloudPC) { Write-Host ' - Licence: Windows 365 detected -- Cloud PC section enabled' -ForegroundColor Cyan } else { Write-Host ' - Licence: No Windows 365 licence -- Cloud PC section will be skipped' -ForegroundColor Yellow } } else { Write-Host ' - Licence: Could not read SKU list -- all sections will attempt to run' -ForegroundColor DarkGray Write-AbrDebugLog 'SKU list empty or unreadable -- defaulting TenantHasIntuneP1/P2 to $true' 'WARN' 'LICENCE' } } catch { Write-Host ' - Licence: SKU check failed -- all sections will attempt to run' -ForegroundColor DarkGray Write-AbrDebugLog "Licence check failed: $($_.Exception.Message)" 'WARN' 'LICENCE' # Keep defaults ($true) so sections still run rather than silently skipping } #endregion #---------------------------------------------------------------------------------------------# # Assignment Resolution Initialisation # #---------------------------------------------------------------------------------------------# # Initialise caches for group name resolution, scope tag resolution, and member counts. # These are populated lazily on first use during section processing. $script:AssignmentResolutionConfig = $script:Options.AssignmentResolution $script:ResolveGroupNames = ($null -eq $script:AssignmentResolutionConfig -or $script:AssignmentResolutionConfig.ResolveGroupNames -ne $false) $script:ShowExcludedGroups = ($null -eq $script:AssignmentResolutionConfig -or $script:AssignmentResolutionConfig.ShowExcludedGroups -ne $false) $script:CheckEmptyGroups = ($null -eq $script:AssignmentResolutionConfig -or $script:AssignmentResolutionConfig.CheckEmptyGroups -ne $false) $script:ResolveScopeTagNames = ($null -eq $script:AssignmentResolutionConfig -or $script:AssignmentResolutionConfig.ResolveScopeTagNames -ne $false) Initialize-IntuneGroupLookup if ($script:ResolveScopeTagNames) { Initialize-IntuneScopeTagLookup } if ($script:ResolveGroupNames) { Write-Host " - Assignment resolution: group names enabled" -ForegroundColor Cyan } if ($script:CheckEmptyGroups) { Write-Host " - Assignment resolution: empty group detection enabled" -ForegroundColor Cyan } #---------------------------------------------------------------------------------------------# # Report Sections # #---------------------------------------------------------------------------------------------# # Tenant Overview if ($InfoLevel.TenantOverview -ge 1) { Write-Host '- Working on Tenant Overview section.' Write-AbrDebugLog 'Starting TenantOverview section' 'DEBUG' 'SECTION' Get-AbrIntuneTenantOverview -TenantId $script:TenantName } # Device Compliance if ($InfoLevel.DeviceCompliance -ge 1) { Write-Host '- Working on Device Compliance section.' Write-AbrDebugLog 'Starting DeviceCompliance section' 'DEBUG' 'SECTION' Get-AbrIntuneComplianceSection -TenantId $script:TenantName } # Configuration Profiles if ($InfoLevel.ConfigurationProfiles -ge 1) { Write-Host '- Working on Configuration Profiles section.' Write-AbrDebugLog 'Starting ConfigurationProfiles section' 'DEBUG' 'SECTION' Get-AbrIntuneConfigurationSection -TenantId $script:TenantName } # App Management if ($InfoLevel.AppManagement -ge 1) { Write-Host '- Working on App Management section.' Write-AbrDebugLog 'Starting AppManagement section' 'DEBUG' 'SECTION' Get-AbrIntuneAppManagementSection -TenantId $script:TenantName } # Enrollment Restrictions if ($InfoLevel.EnrollmentRestrictions -ge 1) { Write-Host '- Working on Enrollment Restrictions section.' Write-AbrDebugLog 'Starting EnrollmentRestrictions section' 'DEBUG' 'SECTION' Get-AbrIntuneEnrollmentSection -TenantId $script:TenantName } # Security Baselines + Endpoint Security if ($InfoLevel.SecurityBaselines -ge 1 -or $InfoLevel.EndpointSecurity -ge 1) { Write-Host '- Working on Security section.' Write-AbrDebugLog 'Starting Security section' 'DEBUG' 'SECTION' Get-AbrIntuneSecuritySection -TenantId $script:TenantName } # Scripts & Remediations if ($InfoLevel.Scripts -ge 1) { Write-Host '- Working on Scripts & Remediations section.' Write-AbrDebugLog 'Starting Scripts section' 'DEBUG' 'SECTION' Get-AbrIntuneScriptsSection -TenantId $script:TenantName } # Managed Devices if ($InfoLevel.Devices -ge 1) { Write-Host '- Working on Managed Devices section.' Write-AbrDebugLog 'Starting Devices section' 'DEBUG' 'SECTION' Get-AbrIntuneDevicesSection -TenantId $script:TenantName } # Windows 365 / Cloud PC if ($InfoLevel.CloudPC -ge 1) { if ($script:TenantHasCloudPC -ne $false) { Write-Host '- Working on Windows 365 / Cloud PC section.' Write-AbrDebugLog 'Starting CloudPC section' 'DEBUG' 'SECTION' Get-AbrIntuneCloudPCSection -TenantId $script:TenantName } else { Write-Host '- Skipping Cloud PC section (no Windows 365 licence detected).' -ForegroundColor Yellow } } # Failed Assignments if ($InfoLevel.FailedAssignments -ge 1) { Write-Host '- Working on Failed Assignments section.' Write-AbrDebugLog 'Starting FailedAssignments section' 'DEBUG' 'SECTION' Get-AbrIntuneFailedAssignmentsSection -TenantId $script:TenantName } #---------------------------------------------------------------------------------------------# # Resolve Output Folder # #---------------------------------------------------------------------------------------------# if ($ReportConfig.Report.OutputFolderPath -and (Test-Path $ReportConfig.Report.OutputFolderPath -ErrorAction SilentlyContinue)) { $script:ResolvedOutputFolder = $ReportConfig.Report.OutputFolderPath } elseif ($OutputFolderPath -and (Test-Path $OutputFolderPath -ErrorAction SilentlyContinue)) { $script:ResolvedOutputFolder = $OutputFolderPath } else { $script:ResolvedOutputFolder = Join-Path $env:USERPROFILE 'Documents' } Write-Host " - Output folder : $($script:ResolvedOutputFolder)" -ForegroundColor Cyan Write-AbrDebugLog "Output folder resolved: $($script:ResolvedOutputFolder)" 'INFO' 'MAIN' #---------------------------------------------------------------------------------------------# # Excel Export # #---------------------------------------------------------------------------------------------# if ($script:ExcelEnabled) { # Consolidate compliance check rows into summary sheets (respects per-sheet toggles) if ((Get-IntuneExcelSheetEnabled -SheetKey 'ACSCe8Assessment') -and $script:E8AllChecks -and $script:E8AllChecks.Count -gt 0) { $script:ExcelSheets['ACSC E8 Assessment'] = $script:E8AllChecks } if ((Get-IntuneExcelSheetEnabled -SheetKey 'CISAssessment') -and $script:CISAllChecks -and $script:CISAllChecks.Count -gt 0) { $script:ExcelSheets['CIS Assessment'] = $script:CISAllChecks } if ($script:ExcelSheets -and $script:ExcelSheets.Count -gt 0) { Write-Host " " Write-Host "- Exporting data to Excel workbook ($($script:ExcelSheets.Count) sheet(s))..." Write-AbrDebugLog "Exporting $($script:ExcelSheets.Count) sheets to Excel" 'INFO' 'EXPORT' $ExcelOutputPath = Join-Path $script:ResolvedOutputFolder ` "Intune_AsBuilt_$($script:TenantDomain)_$(Get-Date -Format 'yyyyMMdd_HHmmss').xlsx" try { Export-IntuneToExcel -Sheets $script:ExcelSheets -Path $ExcelOutputPath -TenantId $script:TenantName Write-AbrDebugLog "Excel export complete: $ExcelOutputPath" 'SUCCESS' 'EXPORT' } catch { Write-Warning "Excel export failed: $($_.Exception.Message)" Write-AbrDebugLog "Excel export failed: $($_.Exception.Message)" 'ERROR' 'EXPORT' Write-TranscriptLog "Excel export failed: $($_.Exception.Message)" 'ERROR' 'EXPORT' } } else { Write-Host "- Excel export: no data collected (all sheets may be disabled or no data found)." -ForegroundColor DarkGray } } else { Write-Host "- Excel export skipped (Options.ExcelExport.Enabled = false)." -ForegroundColor DarkGray Write-AbrDebugLog "Excel export skipped by config" 'INFO' 'EXPORT' } #---------------------------------------------------------------------------------------------# # JSON Backup Export # #---------------------------------------------------------------------------------------------# if ($script:JsonBackupEnabled) { if ($script:BackupData -and $script:BackupData.Count -gt 0) { Write-Host " " Write-Host "- Exporting JSON configuration backup ($($script:BackupData.Count) section(s))..." Write-AbrDebugLog "JSON backup: exporting $($script:BackupData.Count) sections" 'INFO' 'BACKUP' # Export-IntuneBackupJson creates a timestamped subfolder inside BasePath: # <BasePath>/Intune_Backup_<TenantDomain>_<DateTime>/ # _metadata.json -- export info + restore instructions # _manifest.json -- category list with restore endpoints # <Category>/ -- one subfolder per policy category # <PolicyName>.json -- one file per policy, direct POST body Export-IntuneBackupJson ` -BasePath $script:ResolvedOutputFolder ` -TenantName $script:TenantName ` -TenantDomain $script:TenantDomain } else { Write-Host "- JSON backup: no configuration data collected." -ForegroundColor DarkGray } } else { Write-Host "- JSON backup skipped (Options.JsonBackup.Enabled = false)." -ForegroundColor DarkGray } #---------------------------------------------------------------------------------------------# # Clean Up Connections # #---------------------------------------------------------------------------------------------# Write-Host " " Write-Host "- Finished report generation for tenant: $($script:TenantName)" Write-TranscriptLog "Report generation complete for: $($script:TenantName)" 'SUCCESS' 'MAIN' Write-AbrDebugLog "Report generation complete for: $($script:TenantName)" 'SUCCESS' 'MAIN' if ($script:ErrorLog -and $script:ErrorLog.Count -gt 0) { Write-Host " - $($script:ErrorLog.Count) error(s) logged to: $script:ErrorLogPath" -ForegroundColor Yellow } if ($script:Options.KeepSession -eq $true) { Write-Host " - KeepSession is enabled -- Microsoft Graph session retained." -ForegroundColor Yellow Write-AbrDebugLog "KeepSession=true -- skipping disconnect" 'INFO' 'AUTH' } else { Disconnect-IntuneSession } #---------------------------------------------------------------------------------------------# # Debug Log Export # #---------------------------------------------------------------------------------------------# if ($script:DebugLogEnabled -and $script:DebugLogEntries.Count -gt 0) { $script:DebugLogPath = Join-Path $script:ResolvedOutputFolder ` "Intune_DebugLog_$($script:TenantDomain)_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" try { $script:DebugLogEntries | Set-Content -Path $script:DebugLogPath -Encoding UTF8 Write-Host " - Debug log saved to : $($script:DebugLogPath)" -ForegroundColor Cyan } catch { Write-Warning "Could not save debug log: $($_.Exception.Message)" } } # Stop transcript if ($script:TranscriptLogPath) { try { Stop-Transcript -ErrorAction SilentlyContinue | Out-Null Write-Host " - Transcript saved to: $script:TranscriptLogPath" -ForegroundColor Cyan } catch {} } } #endregion foreach Target loop } |