functions/Start-DSCEAscan.ps1

function Start-DSCEAscan {
<#
.SYNOPSIS
Will run Test-DscConfiguration -ReferenceConfiguration against the remote systems supplied and saves the results to a XML file

.DESCRIPTION
Run this function after you have defined the remote systems to scan and have created a localhost.MOF file that defines the settings you want to check against

.NOTES

.LINK
https://microsoft.github.io/DSCEA

.EXAMPLE
. Start-DscEaScan
#>

[CmdletBinding()]
param
    (
        [ValidateNotNullOrEmpty()]
        [string]$OutputPath = '.',

        [ValidateNotNullOrEmpty()]
        [string]$LogsPath = '.',

        [ValidateNotNullOrEmpty()]
        [string]$MofFile = 'localhost.mof',

        [string]$InputFile,

        [ValidateNotNullOrEmpty()]
        [string]$JobTimeout = 600,

        [ValidateNotNullOrEmpty()]
        [string]$ScanTimeout = 3600,

        [switch]$Force,

        [ValidateNotNullOrEmpty()]
        [string]$ResultsFile = "results.$(Get-Date -Format 'yyyyMMdd-HHmm-ss').xml",

        [string[]]$ComputerName,

        [Microsoft.Management.Infrastructure.CimSession[]]$CimSession
    )

    #Begin DSCEA Engine
    Write-Verbose "DSCEA Scan has started"
    $MofFile = (Get-Item $MofFile).FullName
    $runspacePool = [RunspaceFactory]::CreateRunspacePool(1, 10).Open() #Min Runspaces, Max Runspaces
    $scriptBlock = {
        param (
            [ValidateNotNullOrEmpty()]
            [string]$computer,

            [ValidateScript({Test-Path $_ })]
            [string]$mofFile,

            [ValidateNotNullOrEmpty()]
            [string]$JobTimeout,

            [switch]$Force,

            [Microsoft.Management.Infrastructure.CimSession]$CimSession
            )

            function Repair-DSCEngine {
                [CmdletBinding()]
                param
                (
                    [ValidateNotNullOrEmpty()]
                    [string[]]$ComputerName
                )

                #kill the dsc processes on the remote system
                Invoke-Command -ComputerName $ComputerName -ScriptBlock {
        
                    ### find the process that is hosting the DSC engine
                    $dscProcess = Get-WmiObject msft_providers | 
                    Where-Object {$_.provider -like 'dsccore'} | 
                    Select-Object -ExpandProperty HostProcessIdentifier 
        
                    ### Stop the process
                    do {
                        $processID = Get-Process -Id $dscProcess
                        $processID | Stop-Process -Force}
                    while ($processID.ProcessName -match "WmiPrvSE")
                } -ErrorAction SilentlyContinue
            }

        $runTime = Measure-Command {
            try
            {
                if ($PSBoundParameters.ContainsKey('Force')) {
                    for ($i=1; $i -lt 10; $i++) { 
                        Repair-DSCEngine -ComputerName $computer -ErrorAction SilentlyContinue
                    }
                }
                if($PSBoundParameters.ContainsKey('CimSession')) {
                    $DSCJob = Test-DSCConfiguration -ReferenceConfiguration $mofFile -CimSession $CimSession -AsJob | Wait-Job -Timeout $JobTimeout
                }
                else {
                    $DSCJob = Test-DSCConfiguration -ReferenceConfiguration $mofFile -CimSession $computer -AsJob | Wait-Job -Timeout $JobTimeout
                }
                if (!$DSCJob) { 
                    $JobFailedError = "$computer was unable to complete in the alloted job timeout period of $JobTimeout seconds"
                    for ($i=1; $i -lt 10; $i++) { 
                        Repair-DSCEngine -ComputerName $computer -ErrorAction SilentlyContinue
                    }
                    return
                }
                $compliance = Receive-Job $DSCJob -ErrorVariable JobFailedError
                Remove-Job $DSCJob            
            }
            catch {
                $JobFailedError = $_
            } 
        }
        return [PSCustomObject]@{
            Computer = $computer
            RunTime = $runTime
            Compliance = $compliance
            Exception = $JobFailedError
        }
    }

    $jobs = @()
    $results = @()

    if($PSBoundParameters.ContainsKey('CimSession')) {
        $CimSession | ForEach-Object {
            $params = @{
                CimSession = $_
                MofFile = $MofFile
                JobTimeout = $JobTimeout
            }
            if($PSBoundParameters.ContainsKey('Force')) {
                $params += @{Force = $true}
            }
            $job = [Powershell]::Create().AddScript($scriptBlock).AddParameters($params)
            Write-Verbose ('Initiating DSCEA scan on {0}' -f $_.ComputerName)
            $job.RunSpacePool = $runspacePool
            $jobs += [PSCustomObject]@{
                    Pipe = $job
                    Result = $job.BeginInvoke()
            }
        }
    }
    else {
        Write-Verbose "Testing connectivity and PowerShell version of remote systems (All Systems must be running PowerShell 5)"
    
        if($PSBoundParameters.ContainsKey('ComputerName')){
            $firstrunlist = $ComputerName
        }
        else {
            $firstrunlist = Get-Content $InputFile
        }

        $psresults = Invoke-Command -ComputerName $firstrunlist -ErrorAction SilentlyContinue -AsJob -ScriptBlock {
            $PSVersionTable.PSVersion
        } | Wait-Job -Timeout $JobTimeout
        $psjobresults = Receive-Job $psresults

        $runlist =  ($psjobresults | where-object -Property Major -ge 5).PSComputername
        $versionerrorlist =  ($psjobresults | where-object -Property Major -lt 5).PSComputername

        $PSVersionErrorsFile = Join-Path -Path $LogsPath -Childpath ('PSVersionErrors.{0}.xml' -f (Get-Date -Format 'yyyyMMdd-HH:mm:ss'))
    
        Write-Verbose "Connectivity testing complete"
        if ($versionerrorlist){
            Write-Warning "The following systems cannot be scanned as they are not running PowerShell 5. Please check '$versionerrorlist' for details"
        }
        $RunList | Sort-Object | ForEach-Object {
            $params = @{
                Computer = $_
                MofFile = $MofFile
                JobTimeout = $JobTimeout
            }
            if ($PSBoundParameters.ContainsKey('Force')) {
                $params += @{Force = $true}
            }
            $job = [Powershell]::Create().AddScript($scriptBlock).AddParameters($params)
            Write-Verbose "Initiating DSCEA scan on $_"
            $job.RunSpacePool = $runspacePool
            $jobs += [PSCustomObject]@{
                    Pipe = $job
                    Result = $job.BeginInvoke()
            }
        }
    }

    #Wait for Jobs to Complete
    Write-Verbose "Processing Compliance Testing..."
    $overalltimeout = new-timespan -Seconds $ScanTimeout
    $elapsedTime = [system.diagnostics.stopwatch]::StartNew()
    do {
        Start-Sleep -Milliseconds 500
        $jobscomplete = ($jobs.result.iscompleted | Where-Object {$_ -eq $true}).count

        #pecentage complete can be added as the number of jobs completed out of the number of total jobs
        Write-Progress -activity "Working..." -PercentComplete (($jobscomplete / $jobs.count)*100) -status "$([string]::Format("Time Elapsed: {0:d2}:{1:d2}:{2:d2} Jobs Complete: {3} of {4} ", $elapsedTime.Elapsed.hours, $elapsedTime.Elapsed.minutes, $elapsedTime.Elapsed.seconds, $jobscomplete, $jobs.count))";
       
        if ($elapsedTime.elapsed -gt $overalltimeout) {
            Write-Warning "The DSCEA scan was unable to complete because the timeout value of $($overalltimeout.TotalSeconds) seconds was exceeded."
            return
        }
    } while (($jobs.Result.IsCompleted -contains $false) -and ($elapsedTime.elapsed -lt $overalltimeout)) #while elasped time < 1 hour by default

    #Retrieve Jobs
    $jobs | ForEach-Object {
        $results += $_.Pipe.EndInvoke($_.Result)
    }

    ForEach ($exceptionwarning in $results.Exception) {
        Write-Warning $exceptionwarning
    }

    #Save Results
    Write-Verbose "$([string]::Format("Total Scan Time: {0:d2}:{1:d2}:{2:d2}", $elapsedTime.Elapsed.hours, $elapsedTime.Elapsed.minutes, $elapsedTime.Elapsed.seconds))"
    $results | Export-Clixml -Path (Join-Path  -Path $OutputPath -Child $ResultsFile) -Force
    Get-ItemProperty (Join-Path  -Path $OutputPath -Child $ResultsFile)

    #This function will display a divide by zero message if no computers are provided that are runnning PowerShell 5 or above
    if ($versionerrorlist){
        #add in comma separated option for multiple systems
        Write-Warning "The DSCEA scan completed but did not scan all systems. Please check '$PSVersionErrorsFile' for details"
        $versionerrorlist | Export-Clixml -Path $PSVersionErrorsFile -Force
    }

    if ($results.Exception){
        Write-Warning "The DSCEA scan completed but job errors were detected. Please check '$ResultsFile' for details"
    }

}