src/collect/Get-ScoutOutageResource.ps1

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

<#
.SYNOPSIS
    Normalizes Resource Health outage descriptions into cross-platform collector envelopes.

.DESCRIPTION
    The legacy Outages collector used the Windows-only `HTMLFile` COM object merely to turn an
    incident description into text. This pure transform replaces that dependency before report
    processing: it retains the original Resource Graph row and adds the five incident narrative
    fields that the worksheet exposes. No Azure request is made here.

.OUTPUTS
    `AZSC/Monitor/Outage` envelopes with the original row in `properties.SourceResource` and
    the text fields in `properties.DescriptionSections`.
#>

function Get-ScoutOutageResource {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]] $Resources
    )

    function Get-ScoutOutageValue {
        param($InputObject, [string[]] $Path)
        $value = $InputObject
        foreach ($segment in $Path) {
            if ($null -eq $value) { return $null }
            if ($value -is [System.Collections.IDictionary]) {
                if (-not $value.Contains($segment)) { return $null }
                $value = $value[$segment]
                continue
            }
            $property = $value.PSObject.Properties[$segment]
            if ($null -eq $property) { return $null }
            $value = $property.Value
        }
        return $value
    }

    function ConvertTo-ScoutOutageDescriptionSections {
        param([AllowNull()][string] $Description)

        $headings = @(
            'What happened?',
            'What went wrong and why?',
            'How did we respond?',
            'How are we making incidents like this less likely or less impactful?',
            'How can customers make incidents like this less impactful?'
        )
        $result = [ordered]@{}
        foreach ($heading in $headings) { $result[$heading.TrimEnd('?')] = $null }
        if ([string]::IsNullOrWhiteSpace($Description)) { return [pscustomobject] $result }

        # Preserve block boundaries before stripping tags; HTML decode is available on every
        # PowerShell 7 platform and avoids the Windows-only HTMLFile COM dependency.
        $text = $Description -replace '(?is)<(script|style)[^>]*>.*?</\1>', ''
        $text = $text -replace '(?i)<br\s*/?>', "`n"
        $text = $text -replace '(?i)</(?:p|div|li|h[1-6]|tr|section|article)>', "`n"
        $text = $text -replace '(?s)<[^>]+>', ''
        $text = [System.Net.WebUtility]::HtmlDecode($text) -replace "`r`n?", "`n"

        for ($index = 0; $index -lt $headings.Count; $index++) {
            $heading = $headings[$index]
            $start = $text.IndexOf($heading, [System.StringComparison]::OrdinalIgnoreCase)
            if ($start -lt 0) { continue }
            $contentStart = $start + $heading.Length
            $contentEnd = $text.Length
            foreach ($nextHeading in $headings) {
                $next = $text.IndexOf($nextHeading, $contentStart, [System.StringComparison]::OrdinalIgnoreCase)
                if ($next -ge 0 -and $next -lt $contentEnd) { $contentEnd = $next }
            }
            $lines = @($text.Substring($contentStart, $contentEnd - $contentStart) -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ })
            if ($lines.Count -gt 0) { $result[$heading.TrimEnd('?')] = $lines[0] }
        }
        return [pscustomobject] $result
    }

    foreach ($resource in @($Resources)) {
        $type = [string](Get-ScoutOutageValue $resource @('type'))
        if ($type -ine 'Microsoft.ResourceHealth/events') { continue }
        $description = [string](Get-ScoutOutageValue $resource @('properties', 'description'))
        if ($description -notlike '*How can customers make incidents like this less impactful?*') { continue }

        [pscustomobject]@{
            id             = [string](Get-ScoutOutageValue $resource @('id'))
            name           = [string](Get-ScoutOutageValue $resource @('name'))
            type           = 'AZSC/Monitor/Outage'
            subscriptionId = [string](Get-ScoutOutageValue $resource @('subscriptionId'))
            properties     = [pscustomobject]@{
                SourceResource      = $resource
                DescriptionSections = ConvertTo-ScoutOutageDescriptionSections -Description $description
            }
        }
    }
}