Files
M365FoundationsCISReport/source/Public/Export-M365SecurityAuditTable.ps1
2025-04-21 11:57:40 -05:00

201 lines
11 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<#
.SYNOPSIS
Export Microsoft 365 CIS audit results into CSV/Excel and package with hashes.
.DESCRIPTION
Export-M365SecurityAuditTable processes an array of CISAuditResult objects, exporting per-test nested tables
and/or a full audit summary (with oversized fields truncated) to CSV or Excel. All output files are
hashed (SHA256) and bundled into a ZIP archive whose filename includes a short hash for integrity.
.PARAMETER AuditResults
An array of PSCustomObject (CISAuditResult) objects containing the audit results to export or query.
.PARAMETER ExportPath
Path to the directory where CSV/Excel files and the final ZIP archive will be placed. Required for
any file-based export (DefaultExport or OnlyExportNestedTables).
.PARAMETER ExportToExcel
Switch to export files in Excel (.xlsx) format instead of CSV. Requires the ImportExcel module.
.PARAMETER Prefix
A short prefix (05 characters, default 'Corp') appended to the summary audit filename and hashes.
.PARAMETER OnlyExportNestedTables
Switch to export only the per-test nested tables to files, skipping the full audit summary.
.PARAMETER OutputTestNumber
Specify one test number (valid values: '1.1.1','1.3.1','6.1.2','6.1.3','7.3.4') to return that tests
details in-memory as objects without writing any files.
.INPUTS
System.Object[] (array of CISAuditResult PSCustomObjects)
.OUTPUTS
PSCustomObject with property ZipFilePath indicating the final ZIP archive location, or raw test details
when using -OutputTestNumber.
.EXAMPLE
# Return details for test 6.1.2
Export-M365SecurityAuditTable -AuditResults $audits -OutputTestNumber 6.1.2
.EXAMPLE
# Full export (nested tables + summary) to CSV
Export-M365SecurityAuditTable -AuditResults $audits -ExportPath "C:\temp"
.EXAMPLE
# Only export nested tables to Excel
Export-M365SecurityAuditTable -AuditResults $audits -ExportPath "C:\temp" -OnlyExportNestedTables -ExportToExcel
.EXAMPLE
# Custom prefix for filenames
Export-M365SecurityAuditTable -AuditResults $audits -ExportPath "C:\temp" -Prefix Dev
.LINK
https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Export-M365SecurityAuditTable
#>
function Export-M365SecurityAuditTable {
[CmdletBinding(
DefaultParameterSetName = 'DefaultExport',
SupportsShouldProcess,
ConfirmImpact = 'High'
)]
[OutputType([PSCustomObject])]
param(
#───────────────────────────────────────────────────────────────────────────
# 1) DefaultExport: full audit export (nested tables + summary) into ZIP
# -AuditResults, -ExportPath, [-ExportToExcel], [-Prefix]
#───────────────────────────────────────────────────────────────────────────
[Parameter(Mandatory, ParameterSetName = 'DefaultExport')]
[Parameter(Mandatory, ParameterSetName = 'OnlyExportNestedTables')]
[Parameter(Mandatory, ParameterSetName = 'SingleObject')]
[psobject[]]
$AuditResults,
[Parameter(Mandatory, ParameterSetName = 'DefaultExport')]
[Parameter(Mandatory, ParameterSetName = 'OnlyExportNestedTables')]
[string]
$ExportPath,
[Parameter(ParameterSetName = 'DefaultExport')]
[Parameter(ParameterSetName = 'OnlyExportNestedTables')]
[switch]
$ExportToExcel,
[Parameter(ParameterSetName = 'DefaultExport')]
[Parameter(ParameterSetName = 'OnlyExportNestedTables')]
[ValidateLength(0,5)]
[string]
$Prefix = 'Corp',
#───────────────────────────────────────────────────────────────────────────
# 2) OnlyExportNestedTables: nested tables only into ZIP
# -AuditResults, -ExportPath, -OnlyExportNestedTables
#───────────────────────────────────────────────────────────────────────────
[Parameter(Mandatory, ParameterSetName = 'OnlyExportNestedTables')]
[switch]
$OnlyExportNestedTables,
#───────────────────────────────────────────────────────────────────────────
# 3) SingleObject: in-memory output of one tests details
# -AuditResults, -OutputTestNumber
#───────────────────────────────────────────────────────────────────────────
[Parameter(Mandatory, ParameterSetName = 'SingleObject')]
[ValidateSet('1.1.1','1.3.1','6.1.2','6.1.3','7.3.4')]
[string]
$OutputTestNumber
)
Begin {
# Load v4.0 definitions
$AuditResults[0].M365AuditVersion
$script:TestDefinitionsObject = Get-TestDefinition -Version $Version
# Ensure Excel support if requested
if ($ExportToExcel) {
Assert-ModuleAvailability -ModuleName ImportExcel -RequiredVersion '7.8.9'
}
# Tests producing nested tables
$nestedTests = '1.1.1','1.3.1','6.1.2','6.1.3','7.3.4'
# Initialize collections
$results = @()
$createdFiles = [System.Collections.Generic.List[string]]::new()
# Determine which tests to process
if ($PSCmdlet.ParameterSetName -eq 'SingleObject') {
$testsToProcess = @($OutputTestNumber)
} else {
$testsToProcess = $nestedTests
}
}
Process {
foreach ($test in $testsToProcess) {
$item = $AuditResults | Where-Object Rec -EQ $test
if (-not $item) { continue }
switch ($test) {
'6.1.2' { $parsed = Get-AuditMailboxDetail -Details $item.Details -Version '6.1.2' }
'6.1.3' { $parsed = Get-AuditMailboxDetail -Details $item.Details -Version '6.1.3' }
Default { $parsed = $item.Details | ConvertFrom-Csv -Delimiter '|' }
}
$results += [PSCustomObject]@{ TestNumber = $test; Details = $parsed }
}
}
End {
#--- SingleObject: return in-memory details ---
if ($PSCmdlet.ParameterSetName -eq 'SingleObject') {
if ($results.Count -and $results[0].Details) {
return $results[0].Details
}
throw "No results found for test $OutputTestNumber."
}
#--- File export: DefaultExport or OnlyExportNestedTables ---
if (-not $ExportPath) {
throw 'ExportPath is required for file export.'
}
if ($PSCmdlet.ShouldProcess($ExportPath, 'Export and archive audit results')) {
# Ensure directory
if (-not (Test-Path $ExportPath)) { New-Item -Path $ExportPath -ItemType Directory -Force | Out-Null }
$timestamp = (Get-Date).ToString('yyyy.MM.dd_HH.mm.ss')
$exportedTests = @()
# Always truncate large details before writing files
Write-Verbose 'Truncating oversized details...'
$truncatedAudit = Get-ExceededLengthResultDetail `
-AuditResults $AuditResults `
-TestNumbersToCheck $nestedTests `
-ExportedTests $exportedTests `
-DetailsLengthLimit 30000 `
-PreviewLineCount 25
#--- Export nested tables ---
Write-Verbose "[$($PSCmdlet.ParameterSetName)] exporting nested table CSV/XLSX..."
foreach ($entry in $results) {
if (-not $entry.Details) { continue }
$name = "$timestamp`_$($entry.TestNumber)"
$csv = Join-Path $ExportPath "$name.csv"
if ($ExportToExcel) {
$xlsx = [IO.Path]::ChangeExtension($csv, '.xlsx')
$entry.Details | Export-Excel -Path $xlsx -WorksheetName Table -TableName Table -AutoSize -TableStyle Medium2
$createdFiles.Add($xlsx)
} else {
$entry.Details | Export-Csv -Path $csv -NoTypeInformation
$createdFiles.Add($csv)
}
$exportedTests += $entry.TestNumber
}
if ($exportedTests.Count) {
Write-Information "Exported nested tables: $($exportedTests -join ', ')"
} elseif ($OnlyExportNestedTables) {
Write-Warning 'No nested data to export.'
}
#--- Summary export (DefaultExport only) ---
if ($PSCmdlet.ParameterSetName -eq 'DefaultExport') {
Write-Verbose 'Exporting full summary with truncated details...'
$base = "${timestamp}_${Prefix}-M365FoundationsAudit"
$out = Join-Path $ExportPath "$base.csv"
if ($ExportToExcel) {
$xlsx = [IO.Path]::ChangeExtension($out, '.xlsx')
$truncatedAudit | select-object * | Export-Excel -Path $xlsx -WorksheetName Table -TableName Table -AutoSize -TableStyle Medium2
$createdFiles.Add($xlsx)
} else {
Write-Verbose "Exporting to Path: $out"
$truncatedAudit | select-object * | Export-Csv -Path $out -NoTypeInformation
$createdFiles.Add($out)
}
Write-Information 'Exported summary of all audit results.'
}
#--- Hash & ZIP ---
Write-Verbose 'Computing file hashes...'
$hashFile = Join-Path $ExportPath "$timestamp`_${Prefix}-Hashes.txt"
$createdFiles | ForEach-Object {
$h = Get-FileHash -Path $_ -Algorithm SHA256
"$([IO.Path]::GetFileName($_)): $($h.Hash)"
} | Set-Content -Path $hashFile
$createdFiles.Add($hashFile)
Write-Verbose 'Creating ZIP archive...'
$zip = Join-Path $ExportPath "$timestamp`_${Prefix}-M365FoundationsAudit.zip"
Compress-Archive -Path $createdFiles -DestinationPath $zip -Force
$createdFiles | Remove-Item -Force
# Rename to include short hash
$zHash = Get-FileHash -Path $zip -Algorithm SHA256
$final = Join-Path $ExportPath ("$timestamp`_${Prefix}-M365FoundationsAudit_$($zHash.Hash.Substring(0,8)).zip")
Rename-Item -Path $zip -NewName (Split-Path $final -Leaf)
return [PSCustomObject]@{ ZipFilePath = $final }
}
}
}