src/collect/ConvertTo-ScoutDefenderDerivedEvidence.ps1

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

function ConvertTo-ScoutDefenderDerivedEvidence {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]] $Resources
    )

    function Get-DefenderValue {
        param([AllowNull()][object] $InputObject, [Parameter(Mandatory)][string[]] $Paths)
        foreach ($path in $Paths) {
            $current = $InputObject
            foreach ($segment in $path.Split('.')) {
                if ($null -eq $current) { break }
                $property = @($current.PSObject.Properties | Where-Object Name -ieq $segment | Select-Object -First 1)
                if ($property.Count -eq 0) { $current = $null; break }
                $current = $property[0].Value
            }
            if ($null -ne $current) { return $current }
        }
        return $null
    }

    function Get-AssessmentKey {
        param([AllowNull()][object] $Value)
        if ($null -eq $Value) { return $null }
        $text = if ($Value -is [string]) { [string]$Value } else {
            [string](Get-DefenderValue -InputObject $Value -Paths @('Name', 'Id', 'AssessmentKey'))
        }
        if ([string]::IsNullOrWhiteSpace($text)) { return $null }
        return ($text.TrimEnd('/') -split '/')[-1].ToLowerInvariant()
    }

    $sweeps = @($Resources | Where-Object { [string]$_.type -ieq 'AZSC/Subscription/SecurityPolicySweep' })
    $controlMap = @{}
    foreach ($sweep in $sweeps) {
        $subscriptionId = [string](Get-DefenderValue -InputObject $sweep -Paths @('subscriptionId'))
        foreach ($control in @(Get-DefenderValue -InputObject $sweep -Paths @('properties.DefenderSecureScoreControls'))) {
            $definitions = @(Get-DefenderValue -InputObject $control -Paths @(
                    'AssessmentDefinitions',
                    'Definition.AssessmentDefinitions',
                    'Properties.AssessmentDefinitions',
                    'Properties.Definition.AssessmentDefinitions'
                ))
            $maximum = Get-DefenderValue -InputObject $control -Paths @('MaxScore', 'Score.Max', 'Properties.Score.Max')
            $current = Get-DefenderValue -InputObject $control -Paths @('CurrentScore', 'Score.Current', 'Properties.Score.Current')
            $remaining = if ($null -ne $maximum) {
                [math]::Max(0, [double]$maximum - $(if ($null -ne $current) { [double]$current } else { 0 }))
            }
            else { $null }
            foreach ($definition in $definitions) {
                $key = Get-AssessmentKey -Value $definition
                if (-not $key) { continue }
                $controlMap["$subscriptionId|$key"] = [pscustomobject]@{
                    Control = [string](Get-DefenderValue -InputObject $control -Paths @('DisplayName', 'Name', 'Properties.DisplayName'))
                    RemainingScore = $remaining
                }
            }
        }
    }

    $evidence = [System.Collections.Generic.List[object]]::new()
    $unhealthy = [System.Collections.Generic.List[object]]::new()
    foreach ($sweep in $sweeps) {
        $subscriptionId = [string](Get-DefenderValue -InputObject $sweep -Paths @('subscriptionId'))
        foreach ($assessment in @(Get-DefenderValue -InputObject $sweep -Paths @('properties.DefenderAssessments'))) {
            $status = [string](Get-DefenderValue -InputObject $assessment -Paths @('Status.Code', 'Properties.Status.Code'))
            if ($status -notmatch '^(?i:unhealthy)$') { continue }
            $assessmentKey = Get-AssessmentKey -Value (Get-DefenderValue -InputObject $assessment -Paths @('Name', 'Id'))
            $mapping = if ($assessmentKey -and $controlMap.ContainsKey("$subscriptionId|$assessmentKey")) {
                $controlMap["$subscriptionId|$assessmentKey"]
            }
            else { $null }
            $displayName = [string](Get-DefenderValue -InputObject $assessment -Paths @('DisplayName', 'Properties.DisplayName', 'Name'))
            if ([string]::IsNullOrWhiteSpace($displayName)) { $displayName = $assessmentKey }
            $unhealthy.Add([pscustomobject]@{
                    DisplayName = $displayName
                    SubscriptionId = $subscriptionId
                    AssessmentKey = $assessmentKey
                    Severity = [string](Get-DefenderValue -InputObject $assessment -Paths @('Status.Severity', 'Properties.Status.Severity', 'Metadata.Severity'))
                    ResourceId = [string](Get-DefenderValue -InputObject $assessment -Paths @('ResourceDetails.Id', 'Properties.ResourceDetails.Id', 'Id'))
                    Control = if ($mapping) { $mapping.Control } else { $null }
                    RemainingScore = if ($mapping) { $mapping.RemainingScore } else { $null }
                })
        }
    }

    foreach ($group in @($unhealthy | Group-Object DisplayName)) {
        $rows = @($group.Group)
        $mappedControls = @($rows | Where-Object { $null -ne $_.RemainingScore } | Group-Object SubscriptionId, Control | ForEach-Object { $_.Group[0] })
        $impact = if ($mappedControls.Count -gt 0) { [math]::Round([double](($mappedControls | Measure-Object RemainingScore -Sum).Sum), 2) } else { $null }
        $safeName = [uri]::EscapeDataString(($group.Name -replace '/', '_'))
        $evidence.Add([pscustomobject][ordered]@{
                id = "/providers/AzureScout/defender/UnhealthyRecommendationSummary/$safeName"
                name = $group.Name
                type = 'AZSC/Derived/DefenderUnhealthyRecommendation'
                properties = [pscustomobject][ordered]@{
                    DisplayName = $group.Name
                    Severity = (@($rows.Severity | Where-Object { $_ } | Sort-Object -Unique) -join ', ')
                    UnhealthyResourceCount = $rows.Count
                    SubscriptionCount = @($rows.SubscriptionId | Where-Object { $_ } | Sort-Object -Unique).Count
                    SubscriptionIds = @($rows.SubscriptionId | Where-Object { $_ } | Sort-Object -Unique)
                    AssessmentKeys = @($rows.AssessmentKey | Where-Object { $_ } | Sort-Object -Unique)
                    ResourceIds = @($rows.ResourceId | Where-Object { $_ } | Sort-Object -Unique)
                    SecureScoreControls = @($mappedControls | ForEach-Object { $_.Control } | Where-Object { $_ } | Sort-Object -Unique)
                    PotentialScoreImpact = $impact
                    ScoreImpactStatus = if ($mappedControls.Count -gt 0) { 'Calculated from remaining score of linked secure-score controls' } else { 'Unavailable: Azure did not return an assessment-to-control score mapping' }
                }
                AZSC = [pscustomobject][ordered]@{
                    Source = 'Azure Scout derived evidence'
                    Dataset = 'DefenderUnhealthyRecommendations'
                    Operation = 'Group retained unhealthy assessments and link secure-score controls'
                    SourceDatasets = @('DefenderAssessments', 'DefenderSecureScoreControls')
                    CollectedAt = (Get-Date).ToString('o')
                }
            })
    }
    return @($evidence)
}