Public/Import-METReport.ps1
|
function Import-METReport { <# .SYNOPSIS Reads a MET JSON report back into check result objects, without losing tenant provenance. .DESCRIPTION Reads a report file written by Get-METReport -Format JSON (or -Format All) and rebuilds one MET.CheckResult object per saved check, restoring PascalCase property names (CheckId, Category, Result, ...) rather than the camelCase ConvertFrom-Json would otherwise yield straight off the JSON. The report's tenant, run timestamp, and authentication metadata are folded into each result's Metadata (as METRunTenant, METRunTimestamp, and METRunAuthentication) rather than kept at the collection level, so that provenance survives being filtered or concatenated with Where-Object or by piping two imports together - a collection-level stamp would be lost the moment that happened. This lets a saved report be piped straight back into Get-METReport (to regenerate an HTML/console view, or to re-run the cross-tenant provenance check) without re-running the assessment itself. A path that is missing, not valid JSON, or valid JSON without a 'checks' array (i.e. not a MET report) throws a descriptive error rather than returning an empty or partial result set. .PARAMETER Path Path to a JSON report file previously written by Get-METReport -Format JSON or -Format All. .OUTPUTS MET.CheckResult[]. One object per check recorded in the report, with the same shape Invoke-METAssessment produces, so it can be piped into Get-METReport or filtered like any other MET result collection. .EXAMPLE Import-METReport -Path ./assessments/contoso-2026-06-01/MET-report.json Reads a previously saved report back into result objects. .EXAMPLE Import-METReport -Path ./assessments/contoso-2026-06-01/MET-report.json | Get-METReport -Format HTML -OutputPath ./assessments/contoso-2026-06-01/ Regenerates the HTML report from a saved JSON file - for example after an HTML rendering fix ships - without needing a live tenant connection or re-running the assessment. .EXAMPLE $old = Import-METReport -Path ./assessments/contoso-2026-05-01/MET-report.json $new = Import-METReport -Path ./assessments/contoso-2026-06-01/MET-report.json Compare-Object $old $new -Property CheckId, Result Compares two saved runs for the same tenant to see which checks changed result between assessments. .EXAMPLE Get-ChildItem ./assessments -Filter 'MET-report.json' -Recurse | ForEach-Object { Import-METReport -Path $_.FullName } Re-hydrates every saved report under an assessments folder in one pass, for example to build a historical view across many past runs. #> [CmdletBinding(PositionalBinding = $false)] [OutputType('MET.CheckResult')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path ) if (-not (Test-Path -LiteralPath $Path)) { throw "Report file not found: '$Path'" } try { $report = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -ErrorAction Stop } catch { throw "'$Path' is not valid JSON: $($_.Exception.Message)" } if (-not $report.PSObject.Properties['checks'] -or $null -eq $report.checks -or $report.checks -isnot [array]) { throw "'$Path' is not a MET report - it has no 'checks' array. Reports are produced by Get-METReport -Format JSON." } $runTimestamp = $null if ($report.PSObject.Properties['runTimestamp'] -and $report.runTimestamp) { $runTimestamp = [datetime]::Parse( $report.runTimestamp, [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal) } foreach ($check in $report.checks) { # Provenance travels on every result rather than alongside the collection, because # Get-METReport reads it per-result and a collection-level stamp would be lost the # moment someone filtered or concatenated the results (e.g. Where-Object, or piping # two imports together). $metadata = @{} if ($check.PSObject.Properties['metadata'] -and $check.metadata) { foreach ($property in $check.metadata.PSObject.Properties) { $metadata[$property.Name] = $property.Value } } if ($report.PSObject.Properties['tenant'] -and $report.tenant) { $metadata['METRunTenant'] = [string]$report.tenant } if ($runTimestamp) { $metadata['METRunTimestamp'] = $runTimestamp } if ($report.PSObject.Properties['authentication'] -and $report.authentication) { # Left as-is (camelCase, straight off the JSON) rather than reshaped into the # PascalCase $script:METSessionInfo carries for a live session - Get-METReport # reads each shape explicitly by name. Normalizing here would be the same # case-insensitive-access accident C-20 exists to fix, just moved one function over. $metadata['METRunAuthentication'] = $report.authentication } $timestamp = $null if ($check.timestamp) { $timestamp = [datetime]::Parse( $check.timestamp, [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal) } [PSCustomObject]@{ PSTypeName = 'MET.CheckResult' CheckId = [string]$check.checkId Category = [string]$check.category Name = [string]$check.name Result = [string]$check.result Severity = [string]$check.severity Score = $check.score AffectedObject = [string]$check.affectedObject Finding = [string]$check.finding Recommendation = [string]$check.recommendation ReferenceUrl = [string]$check.referenceUrl Timestamp = $timestamp Error = $check.error Metadata = $metadata } } } |