src/assess/Invoke-ScoutComplianceAssessment.ps1

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

<#
.SYNOPSIS
    Run the 'Assess: Compliance' registry entry — score every regulatory-compliance
    initiative assigned in the scanned scope from already-collected policy state.
 
.DESCRIPTION
    The compliance counterpart to Invoke-Assessment (which drives the YAML rule
    engine). This function does not read a rule file: it discovers every BuiltIn
    initiative actually assigned (Resolve-ScoutAssignedInitiative, AB#6794), scores
    each one from collected compliance state (Get-ScoutComplianceScore, AB#6792), and
    returns Findings shaped so the existing Get-Score aggregator — and therefore every
    existing reporter (Html, Excel, ...) — needs no changes to render them: one
    Framework card per initiative+version, three states, never a fabricated pass
    (AB#6793).
 
.PARAMETER Collect
    The assessment collect object.
 
.PARAMETER Assessment
    The manifest key this run was invoked under (for Add-Member tagging, matching
    Invoke-Assessment's contract).
 
.OUTPUTS
    [pscustomobject[]] findings, in the same shape Invoke-Rule/Invoke-Assessment
    produce (Id, Title, Framework, Area, Severity, Status, EvidenceCount, Evidence,
    Remediation, Manual, Assessment, AreaWeight) so Get-Score can aggregate them
    identically to a YAML-rule-scored assessment.
 
.NOTES
    Tracks AB#6792/#6793/#6794 (Feature AB#6744, Epic AB#6454).
#>

function Invoke-ScoutComplianceAssessment {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)] $Collect,
        [string] $Assessment = 'Assess: Compliance'
    )

    $initiatives = @(Resolve-ScoutAssignedInitiative -Collect $Collect)

    if ($initiatives.Count -eq 0) {
        # Nothing observably assigned: honest empty result, not a fabricated pass or a crash.
        # Invoke-ScoutAssessmentCore's RequiresData gate (same mechanism as SMART, AB#6832)
        # should normally keep this from being reached with zero rows at all, but a tenant that
        # HAS compliance rows for zero BuiltIn initiatives (e.g. only custom policy) reaches here
        # legitimately, and must produce zero findings, not throw.
        Write-Warning "Invoke-ScoutComplianceAssessment: no BuiltIn regulatory-compliance initiative is observably assigned in the scanned scope (policyComplianceStates has rows, but none match a known BuiltIn policySetDefinition) — nothing to score."
        return @()
    }

    $findings = foreach ($initiative in $initiatives) {
        # AB#6792 -- an initiative Scout could not confirm is BuiltIn is NOT scored (scoring a
        # customer's custom initiative under a framework's name would be actively misleading),
        # but it is NOT silently dropped either. It surfaces as a single Not assessed finding
        # naming the initiative, so an operator sees "this is assigned and I could not evaluate
        # it" rather than an empty report. A live run against tenant tppoc found the old
        # behaviour returning ZERO findings against 469 real compliance rows.
        if ($initiative.PSObject.Properties['Unconfirmed'] -and $initiative.Unconfirmed) {
            [pscustomobject]@{
                Id            = "COMPLIANCE-UNCONFIRMED-$($initiative.Id -replace '^.*/', '')"
                Title         = "Initiative $($initiative.Id) is assigned but could not be confirmed as an Azure built-in"
                Framework     = 'Regulatory compliance (unconfirmed initiative)'
                Area          = 'Regulatory compliance'
                Severity      = $null
                Status        = 'NotAssessed'
                EvidenceCount = 0
                Evidence      = @()
                Remediation   = 'Scout found policy compliance rows for this initiative but no matching built-in policy set definition, so it cannot say which framework the result belongs to. Either the policy set definition sweep did not run for this scope, or the initiative is a custom one. Confirm the definitions collection ran, and grant Reader at the scope that holds the definition.'
                Manual        = $false
            } | Add-Member -NotePropertyName Assessment -NotePropertyValue $Assessment -PassThru |
                Add-Member -NotePropertyName AreaWeight -NotePropertyValue 1.0 -PassThru |
                Add-Member -NotePropertyName InitiativeId -NotePropertyValue $initiative.Id -PassThru |
                Add-Member -NotePropertyName InitiativeVersion -NotePropertyValue 'N/A' -PassThru
            continue
        }
        $scored = Get-ScoutComplianceScore -Collect $Collect -Initiative $initiative
        foreach ($f in $scored.Findings) {
            [pscustomobject]@{
                Id            = $f.Id
                Title         = $f.Title
                Framework     = $scored.Framework
                Area          = 'Regulatory compliance'
                Severity      = $null
                Status        = $f.Status
                EvidenceCount = $f.EvidenceCount
                Evidence      = $f.Evidence
                Remediation   = $f.Remediation
                Manual        = $false
            } | Add-Member -NotePropertyName Assessment -NotePropertyValue $Assessment -PassThru |
                Add-Member -NotePropertyName AreaWeight -NotePropertyValue 1.0 -PassThru |
                Add-Member -NotePropertyName InitiativeId -NotePropertyValue $scored.InitiativeId -PassThru |
                Add-Member -NotePropertyName InitiativeVersion -NotePropertyValue $scored.Version -PassThru
        }
    }

    return @($findings)
}