Private/Invoke-ArgusCheckRunner.ps1

function Invoke-ArgusCheckRunner {
    <#
    .SYNOPSIS
        Runs a set of check definitions and returns one result record per check.

    .DESCRIPTION
        For each check, in order:
        1. filters (-Area, -Id, -Tag, -ExcludeId) and config 'Enabled' -> NotRun with the reason;
        2. Requires (services, scopes, licences) -> Skipped with the reason, before execution;
        3. data sets of the remaining checks are prefetched once (Get-ArgusData); a check whose data set
           failed to load -> Error;
        4. the Test scriptblock runs isolated in try/catch -> Error with the message on exception;
        5. global exclusions are removed from the findings (unless ApplyExclusions = $false), counts are
           adjusted, and placeholders in Title / Summary are resolved.
        Checks run sequentially (v0.1).
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]] $Check,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Config,

        [Parameter(Mandatory)]
        [pscustomobject] $Environment,

        [Parameter()]
        [string[]] $Area,

        [Parameter()]
        [string[]] $Id,

        [Parameter()]
        [string[]] $ExcludeId,

        [Parameter()]
        [string[]] $Tag,

        [Parameter()]
        [datetime] $Now = [datetime]::UtcNow
    )

    $matchesAny = { param($Value, $Patterns) [bool]($Patterns | Where-Object { $Value -like $_ }) }

    $knownIds = @($Check | ForEach-Object { $_.Id })
    foreach ($configuredId in $Config.Checks.Keys) {
        if ($configuredId -notin $knownIds) {
            Write-Warning "Configuration contains settings for unknown check '$configuredId'; they are ignored."
        }
    }

    # 1-2. Selection and prerequisites
    $plan = foreach ($definition in $Check) {
        $setting = Resolve-ArgusCheckSetting -Check $definition -Config $Config
        $notRunReason = if ($Area -and $definition.Area -notin $Area) { 'Not selected by the -Area filter.' }
        elseif ($Id -and -not (& $matchesAny $definition.Id $Id)) { 'Not selected by the -Id filter.' }
        elseif ($Tag -and -not ($definition.Tags | Where-Object { $_ -in $Tag })) { 'Not selected by the -Tag filter.' }
        elseif ($ExcludeId -and (& $matchesAny $definition.Id $ExcludeId)) { 'Excluded with -ExcludeId.' }
        elseif (-not $setting.Enabled) { 'Disabled in configuration.' }

        [string[]] $skipReasons = @()
        if (-not $notRunReason) { $skipReasons = @(Test-ArgusRequirement -Check $definition -Environment $Environment) }
        [pscustomobject]@{ Check = $definition; Setting = $setting; NotRunReason = $notRunReason; SkipReasons = $skipReasons }
    }
    $plan = @($plan)
    $runnable = @($plan | Where-Object { -not $_.NotRunReason -and $_.SkipReasons.Count -eq 0 })

    # 3. Prefetch data sets once
    $data = @{}
    $dataErrors = @{}
    $dataSetNames = @($runnable | ForEach-Object { $_.Check.Requires.DataSets } | Where-Object { $_ } | Sort-Object -Unique)
    for ($i = 0; $i -lt $dataSetNames.Count; $i++) {
        $name = $dataSetNames[$i]
        Write-Progress -Id 1 -Activity 'ARGUS: loading tenant data' -Status $name -PercentComplete (100 * $i / $dataSetNames.Count)
        try {
            $data[$name] = Get-ArgusData -Name $name -ErrorAction Stop
        }
        catch {
            $dataErrors[$name] = $_.Exception.Message
        }
    }
    Write-Progress -Id 1 -Activity 'ARGUS: loading tenant data' -Completed

    # Optional data sets: loaded only when their service is connected; a failure is a note, not an error.
    $serviceOf = @{}
    foreach ($dataSet in Get-ArgusDataSetDefinition) { $serviceOf[$dataSet.Name] = $dataSet.Service }
    $optionalNames = @($runnable | ForEach-Object { $_.Check.Requires.OptionalDataSets } | Where-Object { $_ -and -not $data.ContainsKey($_) } | Sort-Object -Unique)
    foreach ($name in $optionalNames) {
        $service = $serviceOf[$name]
        if ($service -and $service -notin @($Environment.ConnectedServices)) { continue }
        try {
            $data[$name] = Get-ArgusData -Name $name -ErrorAction Stop
        }
        catch {
            Add-ArgusDataSetNote -Name $name -Text "Optional data set not loaded: $($_.Exception.Message)"
        }
    }

    $hasExclusions = [bool]($Config.Exclusions.Values | ForEach-Object { $_ } | Where-Object { $_ })

    # 4-5. Execute
    for ($i = 0; $i -lt $plan.Count; $i++) {
        $item = $plan[$i]
        $definition = $item.Check
        $parameters = $item.Setting.Parameters
        Write-Progress -Id 2 -Activity 'ARGUS: running checks' -Status "$($definition.Id) $($definition.Title)" -PercentComplete (100 * $i / [math]::Max(1, $plan.Count))

        $result = $null
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $failedDataSets = @($definition.Requires.DataSets | Where-Object { $dataErrors.ContainsKey($_) })

        if ($item.NotRunReason) {
            $result = New-ArgusResult -Status NotRun -SkipReason $item.NotRunReason
        }
        elseif ($item.SkipReasons.Count -gt 0) {
            $result = New-ArgusResult -Status Skipped -SkipReason (($item.SkipReasons -join '; ') + '.')
        }
        elseif ($failedDataSets.Count -gt 0) {
            $messages = $failedDataSets | ForEach-Object { "Data set '$_' could not be loaded: $($dataErrors[$_])" }
            $result = New-ArgusResult -Status Error -ErrorMessage ($messages -join ' ')
        }
        else {
            $checkData = @{}
            foreach ($name in $definition.Requires.DataSets) { $checkData[$name] = $data[$name] }
            foreach ($name in $definition.Requires.OptionalDataSets) { if ($data.ContainsKey($name)) { $checkData[$name] = $data[$name] } }
            $context = [pscustomobject]@{
                CheckId     = $definition.Id
                Config      = $Config
                Data        = $checkData
                Parameters  = $parameters.Clone()
                Now         = $Now
                Environment = $Environment
            }
            $previousPreference = $ErrorActionPreference
            try {
                # Non-terminating errors inside a check become terminating, so they surface as Error.
                $ErrorActionPreference = 'Stop'
                $output = @(& $definition.Test $context)
                $returned = @($output | Where-Object { $null -ne $_ -and $_.PSObject.TypeNames -contains 'Argus.Result' })
                if ($returned.Count -ne 1) {
                    throw "The check must return exactly one result created with New-ArgusResult (returned $($returned.Count))."
                }
                $result = $returned[0]
            }
            catch {
                $result = New-ArgusResult -Status Error -ErrorMessage $_.Exception.Message
            }
            finally {
                $ErrorActionPreference = $previousPreference
            }
        }
        $stopwatch.Stop()

        # Global exclusions
        $excludedCount = 0
        if ($hasExclusions -and $definition.ApplyExclusions -and @($result.Findings).Count -gt 0) {
            $kept = @($result.Findings | Where-Object { -not (Test-ArgusExcluded -InputObject $_ -Exclusions $Config.Exclusions) })
            $excludedCount = @($result.Findings).Count - $kept.Count
            if ($excludedCount -gt 0) {
                $result.Findings = $kept
                $result.AffectedCount = [math]::Max(0, $result.AffectedCount - $excludedCount)
                if ($null -ne $result.EvaluatedCount) {
                    $result.EvaluatedCount = [math]::Max(0, $result.EvaluatedCount - $excludedCount)
                }
                if ($result.Status -in 'Failed', 'Investigate' -and $kept.Count -eq 0 -and $result.AffectedCount -eq 0) {
                    $result.Status = 'Passed'
                }
            }
        }

        $values = @{}
        foreach ($key in $parameters.Keys) { $values[$key] = $parameters[$key] }
        $values['AffectedCount'] = $result.AffectedCount
        $values['EvaluatedCount'] = $result.EvaluatedCount
        $values['ExcludedCount'] = $excludedCount

        $summary = Resolve-ArgusTemplate -Template $result.Summary -Values $values
        if ($excludedCount -gt 0 -and $result.Summary -notmatch '\{ExcludedCount\}') {
            $summary = ("$summary " + (Resolve-ArgusTemplate -Template '{ExcludedCount} object(s) excluded by configuration.' -Values $values)).Trim()
        }

        [pscustomobject]@{
            PSTypeName      = 'Argus.CheckResult'
            Id              = $definition.Id
            Title           = Resolve-ArgusTemplate -Template $definition.Title -Values $parameters
            Area            = $definition.Area
            AreaDisplayName = $script:ArgusAreas[$definition.Area].DisplayName
            Category        = $definition.Category
            Severity        = $item.Setting.Severity
            Status          = $result.Status
            Summary         = $summary
            AffectedCount   = $result.AffectedCount
            EvaluatedCount  = $result.EvaluatedCount
            ExcludedCount   = $excludedCount
            Findings        = @($result.Findings)
            FindingColumns  = @($result.FindingColumns)
            SkipReason      = $result.SkipReason
            ErrorMessage    = $result.ErrorMessage
            Duration        = [math]::Round($stopwatch.Elapsed.TotalSeconds, 3)
            Parameters      = $parameters
            Description     = Resolve-ArgusTemplate -Template $definition.Description -Values $parameters
            Rationale       = $definition.Rationale
            Remediation     = $definition.Remediation
            References      = @($definition.References)
            Requires        = $definition.Requires
            Tags            = @($definition.Tags)
            UsesBeta        = $definition.UsesBeta
            IsCustom        = -not ($definition.Source -and $definition.Source.StartsWith($script:ArgusModuleRoot, [System.StringComparison]::OrdinalIgnoreCase))
        }
    }
    Write-Progress -Id 2 -Activity 'ARGUS: running checks' -Completed
}