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, restart and update state, storage, applications,
    scheduled jobs, files, certificates, events, processes, local Windows management
    information, local network placement, account relationships, and approved network services. This command does
    not apply a fix or change Windows.
 
    TcpPort, DnsResolution, HttpEndpointHealth, WindowsUpdateAvailable, and
    LocalGroupMembership are network-active. They are blocked unless AllowNetworkChecks is
    supplied. Contacted services, identity providers, or monitoring tools may record the
    activity. Update scans never install updates or change update settings.
 
    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 every target and network-active item before running it.
 
    .PARAMETER ControlId
    One or more checklist item IDs to check. Every item is checked by default.
 
    .PARAMETER AllowNetworkChecks
    Allows the five network-active types after their destinations, requested account
    identities, update options, and purposes have been reviewed. This is an explicit
    acknowledgement, not a network authorization system.
 
    .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 -AllowNetworkChecks -NoProgress
 
    Runs the expanded report-only examples, including approved network-active items.
 
    .OUTPUTS
    EndpointForge.ComplianceReport
    #>

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

        [string[]]$ControlId,

        [switch]$AllowNetworkChecks,

        [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) })
    }

    $networkControls = @($controls | Where-Object { Test-EFControlUsesNetwork -Control $_ })
    if ($networkControls.Count -gt 0 -and -not $AllowNetworkChecks) {
        $networkTypes = @($networkControls.Type | Sort-Object -Unique) -join ', '
        throw [System.InvalidOperationException]::new(
            "This checklist contains $($networkControls.Count) network-active item(s): $networkTypes. " +
            'Review their destinations and purpose, then add -AllowNetworkChecks to run them.'
        )
    }

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

    $controlIndex = 0
    $baselineCommandArgument = Get-EFBaselineCommandArgument -Baseline $resolvedBaseline
    $evaluationContext = @{
        AllowNetworkChecks = [bool]$AllowNetworkChecks
        Cache              = @{}
    }
    $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 -EvaluationContext $evaluationContext
            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
        NetworkCheckCount   = $networkControls.Count
        NetworkChecksAllowed = [bool]$AllowNetworkChecks
        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
}