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
}