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, 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()]
    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
}