Public/Get-EFComplianceReport.ps1

function Get-EFComplianceReport {
    <#
    .SYNOPSIS
    Checks this computer against a selected checklist without applying fixes.
 
    .DESCRIPTION
    Reads each selected item and compares it with the expected answer in a checklist.
    Items can cover Windows settings, an exact local file, literal text near the end of a
    log, recent Windows event IDs, or one TCP host and port. This command does not apply a
    fix or change Windows. A TcpPort item does make one real, observable connection
    attempt to the named destination, then closes it without sending application data.
 
    File-text results do not contain matching lines, and event results do not contain
    event messages or event data. In script output, a checklist is called a baseline and
    each checklist item is called a control. Those names are retained so existing
    PowerShell automation remains compatible.
 
    The report includes results, a score, guidance, and a predictable ExitCode for
    scripts: 0 when checked items match, 2 when an item does not match, or 3 when one or
    more items could not be checked.
 
    Use Test-EFEndpointCompliance when a simple Boolean answer is needed.
 
    .PARAMETER Baseline
    The checklist to use: a built-in name, a checklist JSON file, or an object returned
    by Get-EFBaseline. Review custom paths, event queries, hosts, and ports before running
    the checklist.
 
    .PARAMETER ControlId
    One or more checklist item IDs to check. Every item is checked by default.
 
    .PARAMETER NoProgress
    Suppresses the progress display for non-interactive automation hosts.
 
    .EXAMPLE
    Get-EFComplianceReport
 
    .EXAMPLE
    $report = Get-EFComplianceReport -Baseline .\contoso-baseline.json
    $report.Results | Where-Object Status -ne 'Compliant'
 
    .EXAMPLE
    Get-EFComplianceReport -Baseline .\Contoso.EverydayChecks.json -NoProgress
 
    Runs the four report-only everyday checks in the custom file. If it includes TcpPort,
    the named connection attempt can be recorded by the destination or network tools.
 
    .OUTPUTS
    EndpointForge.ComplianceReport
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [AllowNull()]
        [object]$Baseline = 'EnterpriseRecommended',

        [string[]]$ControlId,

        [switch]$NoProgress
    )

    $null = Test-EFWindows -Throw
    $correlationId = [guid]::NewGuid().ToString()
    $resolvedBaseline = Resolve-EFBaseline -Baseline $Baseline
    $controls = @($resolvedBaseline.Controls)

    if ($PSBoundParameters.ContainsKey('ControlId') -and $null -ne $ControlId -and $ControlId.Length -gt 0) {
        $missingIds = @($ControlId | Where-Object { $_ -notin $controls.Id })
        if ($missingIds.Count -gt 0) {
            throw [System.ArgumentException]::new("Unknown control Id(s): $($missingIds -join ', ')")
        }
        $controls = @($controls | Where-Object { $_.Id -in @($ControlId) })
    }

    Write-EFLog -Message "Compliance evaluation started for baseline '$($resolvedBaseline.Name)'." `
        -CorrelationId $correlationId -Data @{ controlCount = $controls.Count }

    $controlIndex = 0
    $baselineCommandArgument = Get-EFBaselineCommandArgument -Baseline $resolvedBaseline
    $results = @(
        foreach ($control in $controls) {
            $controlIndex++
            if (-not $NoProgress) {
                Write-Progress -Id 1102 -Activity 'EndpointForge checklist check' `
                    -Status "Checking: $($control.Title)" `
                    -PercentComplete ([math]::Round(($controlIndex / $controls.Count) * 100))
            }
            $controlResult = Get-EFControlState -Control $control
            if ($controlResult.Status -eq 'NonCompliant' -and $controlResult.Remediable) {
                $controlResult.RecommendedAction = if ($null -ne $baselineCommandArgument) {
                    "Preview this supported fix before approval. For scripts: Invoke-EFEndpointRemediation $baselineCommandArgument -ControlId '$($controlResult.ControlId)' -WhatIf"
                }
                else {
                    'Create a fix plan from this same in-memory checklist, then preview the supported change before approval.'
                }
            }
            $controlResult
        }
    )
    if (-not $NoProgress) {
        Write-Progress -Id 1102 -Activity 'EndpointForge checklist check' -Completed
    }

    $compliantCount = @($results | Where-Object Status -eq 'Compliant').Count
    $nonCompliantCount = @($results | Where-Object Status -eq 'NonCompliant').Count
    $notApplicableCount = @($results | Where-Object Status -eq 'NotApplicable').Count
    $errorCount = @($results | Where-Object Status -eq 'Error').Count
    $knownCount = $compliantCount + $nonCompliantCount
    $attemptedCount = $knownCount + $errorCount
    $score = if ($knownCount -eq 0) { 100 } else { [math]::Round(($compliantCount / $knownCount) * 100, 1) }
    $coveragePercent = if ($attemptedCount -eq 0) { 100 } else { [math]::Round(($knownCount / $attemptedCount) * 100, 1) }
    $dataStatus = if ($errorCount -eq 0) { 'Complete' } elseif ($knownCount -eq 0) { 'Failed' } else { 'Partial' }
    $isCompliant = $nonCompliantCount -eq 0 -and $errorCount -eq 0
    $exitCode = if ($errorCount -gt 0) { 3 } elseif ($nonCompliantCount -gt 0) { 2 } else { 0 }
    $status = if ($nonCompliantCount -gt 0) { 'NonCompliant' } elseif ($errorCount -gt 0) { 'Incomplete' } else { 'Compliant' }
    $automaticFixCount = @($results | Where-Object { $_.Status -eq 'NonCompliant' -and $_.Remediable }).Count
    $summaryText = if ($isCompliant) {
        "$compliantCount applicable checklist item(s) have the expected result."
    }
    elseif ($errorCount -gt 0) {
        "The check is incomplete: $nonCompliantCount item(s) do not match and $errorCount could not be checked."
    }
    else {
        "$nonCompliantCount checklist item(s) need attention. $score% of the items that could be checked match."
    }
    $nextStep = if ($isCompliant) {
        'No supported fix is needed.'
    }
    elseif ($errorCount -gt 0) {
        'Review the items that could not be checked. Some protected information requires PowerShell to be opened with Run as Administrator (also called an elevated session).'
    }
    elseif ($automaticFixCount -gt 0) {
        'Create a fix plan to see what EndpointForge can safely preview and what needs a person to review.'
    }
    else {
        'Review the details and follow your organization''s approved manual guidance. EndpointForge does not change these report-only items.'
    }

    $report = [pscustomobject]@{
        PSTypeName          = 'EndpointForge.ComplianceReport'
        ComputerName        = $env:COMPUTERNAME
        BaselineName        = [string]$resolvedBaseline.Name
        BaselineVersion     = [string]$resolvedBaseline.Version
        ChecklistName       = [string]$resolvedBaseline.Name
        ChecklistVersion    = [string]$resolvedBaseline.Version
        ChecklistItemCount  = $controls.Count
        CorrelationId       = $correlationId
        EvaluatedAtUtc      = [DateTime]::UtcNow
        IsCompliant         = $isCompliant
        Status              = $status
        Score               = $score
        DataStatus          = $dataStatus
        CoveragePercent     = $coveragePercent
        ExitCode            = $exitCode
        Summary             = $summaryText
        NextStep            = $nextStep
        CompliantCount      = $compliantCount
        NonCompliantCount   = $nonCompliantCount
        NotApplicableCount  = $notApplicableCount
        ErrorCount          = $errorCount
        AutomaticFixCount   = $automaticFixCount
        Results             = $results
    }

    Write-EFLog -Message "Compliance evaluation completed for baseline '$($resolvedBaseline.Name)'." `
        -Level $(if ($isCompliant) { 'Information' } else { 'Warning' }) -CorrelationId $correlationId `
        -Data @{ score = $score; exitCode = $exitCode; nonCompliant = $nonCompliantCount; errors = $errorCount }

    return $report
}