Common/Export-AssessmentReport.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Generates an HTML assessment report from M365 assessment output. .DESCRIPTION Reads CSV data from an M365 assessment output folder and produces a self-contained HTML report powered by a React single-page application. The report bundles all JavaScript, CSS, and data inline — no external files or CDN calls required. A companion XLSX compliance matrix is also generated in the same folder. .PARAMETER AssessmentFolder Path to the assessment output folder (e.g., .\M365-Assessment\Assessment_20260306_195618). Must contain _Assessment-Summary.csv. .PARAMETER OutputPath Path for the generated HTML report. Defaults to _Assessment-Report_<domain>.html in the assessment folder. .PARAMETER TenantName Tenant display name for the report title. Read from Tenant Information CSV if omitted. .PARAMETER WhiteLabel Hides M365-Assess GitHub link and Galvnyz attribution from the report footer. .PARAMETER OpenReport Automatically opens the generated HTML report in the default browser. .PARAMETER QuickScan Passed through for context; has no effect on the React HTML report. .PARAMETER DriftReport Drift comparison rows from Compare-AssessmentBaseline. Passed to the XLSX export. .PARAMETER DriftBaselineLabel Baseline label string — retained for downstream compatibility. .PARAMETER DriftBaselineTimestamp Baseline timestamp string — retained for downstream compatibility. .EXAMPLE PS> .\Common\Export-AssessmentReport.ps1 -AssessmentFolder '.\M365-Assessment\Assessment_20260306_195618' .NOTES Author: Daren9m #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$AssessmentFolder, [Parameter()] [string]$OutputPath, [Parameter()] [string]$TenantName, [Parameter()] [switch]$WhiteLabel, [Parameter()] [switch]$CompactReport, [Parameter()] [switch]$OpenReport, [Parameter()] [switch]$QuickScan, [Parameter()] [AllowEmptyCollection()] [PSCustomObject[]]$DriftReport = @(), [Parameter()] [string]$DriftBaselineLabel = '', [Parameter()] [string]$DriftBaselineTimestamp = '' ) $ErrorActionPreference = 'Stop' $projectRoot = Split-Path -Parent (Split-Path -Parent $PSCommandPath) # ------------------------------------------------------------------ # Load control registry and framework definitions # ------------------------------------------------------------------ . (Join-Path -Path $PSScriptRoot -ChildPath 'Import-ControlRegistry.ps1') $controlsPath = Join-Path -Path $projectRoot -ChildPath 'controls' $cisFrameworkId = 'cis-m365-v6' $controlRegistry = Import-ControlRegistry -ControlsPath $controlsPath -CisFrameworkId $cisFrameworkId . (Join-Path -Path $PSScriptRoot -ChildPath 'Import-FrameworkDefinitions.ps1') $allFrameworks = Import-FrameworkDefinitions -FrameworksPath (Join-Path -Path $projectRoot -ChildPath 'controls/frameworks') # ------------------------------------------------------------------ # Validate input # ------------------------------------------------------------------ if (-not (Test-Path -Path $AssessmentFolder -PathType Container)) { Write-Error "Assessment folder not found: $AssessmentFolder" return } $summaryFile = Get-ChildItem -Path $AssessmentFolder -Filter '_Assessment-Summary*.csv' -ErrorAction SilentlyContinue | Select-Object -First 1 $summaryPath = if ($summaryFile) { $summaryFile.FullName } else { Join-Path -Path $AssessmentFolder -ChildPath '_Assessment-Summary.csv' } if (-not (Test-Path -Path $summaryPath)) { Write-Error "Summary CSV not found: $summaryPath" return } # ------------------------------------------------------------------ # Load assessment metadata # ------------------------------------------------------------------ $summary = Import-Csv -Path $summaryPath $tenantCsv = Join-Path -Path $AssessmentFolder -ChildPath '01-Tenant-Info.csv' $tenantData = if (Test-Path -Path $tenantCsv) { Import-Csv -Path $tenantCsv } else { $null } if (-not $TenantName) { if ($tenantData -and @($tenantData).Count -gt 0 -and $tenantData[0].PSObject.Properties.Name -contains 'OrgDisplayName') { $TenantName = $tenantData[0].OrgDisplayName } elseif ($tenantData -and @($tenantData).Count -gt 0 -and $tenantData[0].PSObject.Properties.Name -contains 'DefaultDomain') { $TenantName = $tenantData[0].DefaultDomain } else { $TenantName = 'M365 Tenant' } } # Read domain prefix and version from the assessment log $reportDomainPrefix = '' $assessmentVersion = (Import-PowerShellDataFile -Path "$PSScriptRoot/../M365-Assess.psd1").ModuleVersion $logFile = Get-ChildItem -Path $AssessmentFolder -Filter '_Assessment-Log*.txt' -ErrorAction SilentlyContinue | Select-Object -First 1 $logPath = if ($logFile) { $logFile.FullName } else { Join-Path -Path $AssessmentFolder -ChildPath '_Assessment-Log.txt' } if (Test-Path -Path $logPath) { $logHead = Get-Content -Path $logPath -TotalCount 10 $versionLine = $logHead | Where-Object { $_ -match 'Version:\s+v(.+)' } if ($versionLine) { $assessmentVersion = $Matches[1] } $domainLine = $logHead | Where-Object { $_ -match 'Domain:\s+(\S+)' } if ($domainLine -and $Matches[1]) { $reportDomainPrefix = $Matches[1].Trim() } } # Determine output path if (-not $OutputPath) { $suffix = if ($reportDomainPrefix) { "_$reportDomainPrefix" } else { '' } $OutputPath = Join-Path -Path $AssessmentFolder -ChildPath "_Assessment-Report$suffix.html" } # ------------------------------------------------------------------ # Load section data, build findings list, and export XLSX # ------------------------------------------------------------------ . (Join-Path -Path $PSScriptRoot -ChildPath 'Build-ReportData.ps1') . (Join-Path -Path $PSScriptRoot -ChildPath 'Build-SectionHtml.ps1') # $allCisFindings and $sectionData are now set in scope # ------------------------------------------------------------------ # Build REPORT_DATA JSON # ------------------------------------------------------------------ $xlsxName = if ($reportDomainPrefix) { "_Compliance-Matrix_$reportDomainPrefix.xlsx" } else { '_Compliance-Matrix.xlsx' } $reportTitle = if ($TenantName -ne 'M365 Tenant') { "$TenantName — M365 Security Assessment" } else { 'M365 Security Assessment' } $reportJson = Build-ReportDataJson ` -AllFindings $allCisFindings ` -SectionData $sectionData ` -RegistryData $controlRegistry ` -WhiteLabel: $WhiteLabel ` -XlsxFileName $xlsxName ` -FrameworkDefs $allFrameworks # ------------------------------------------------------------------ # Assemble HTML and write output # ------------------------------------------------------------------ . (Join-Path -Path $PSScriptRoot -ChildPath 'Get-ReportTemplate.ps1') $html = Get-ReportTemplate -ReportDataJson $reportJson -ReportTitle $reportTitle Set-Content -Path $OutputPath -Value $html -Encoding UTF8 Write-Output "HTML report generated: $OutputPath" if ($OpenReport) { Start-Process -FilePath $OutputPath } |