functions/Invoke-AhsCheck.ps1

function Invoke-AhsCheck {
    <#
    .SYNOPSIS
        Verify AD object health, based on the configuration provided.
     
    .DESCRIPTION
        Verify AD object health, based on the configuration provided.
        This applies the registered and configured checks against corresponding objects in AD.
 
        For more details on how to define checks, see the help on Register-AhsCheck.
         
        The configuration - whether provided through hashtable or config file - looks like this:
 
        @{
            NameOfCheck1 = @{ NameOfParameter = 42 }
            NameOfCheck2 = @{ } # Execute with default parameter settings
        }
         
        Only configured checks will be executed, even if explicitly specifying the "-IncludeCheck" parameter.
        To apply _all_ checks no matter what, specify the "_All" option:
 
        @{
            _All = $true # Execute all checks, including those no configuration is provided for
            NameOfCheck1 = @{ NameOfParameter = 42 }
            NameOfCheck2 = @{ SomeThreshold = 128 }
        }
 
        By default, all object classes that apply to a check are used.
        To limit that, also provide the "_ObjectClasses" option:
 
        @{
            _All = $true # Execute all checks, including those no configuration is provided for
            _ObjectClasses = 'Person', 'Group' # Only execute checks against persons and groups.
            NameOfCheck1 = @{ NameOfParameter = 42 }
            NameOfCheck2 = @{ SomeThreshold = 128 }
        }
     
    .PARAMETER Configuration
        A configuration hashtable, defining how the scan should be performed.
        See the Description for notes on how this should be defined.
     
    .PARAMETER ConfigFile
        Path to a configuration file to read.
        See the Description for notes on how this should be defined.
     
    .PARAMETER All
        Rather than loading a specific config file, execute al available checks with the default configuration.
     
    .PARAMETER IncludeCheck
        Only execute these checks.
     
    .PARAMETER ExcludeCheck
        Do not execute these checks.
     
    .PARAMETER IncludeClass
        Only execute checks against these object classes.
     
    .PARAMETER ExcludeClass
        Do not execute checks against these object classes.
     
    .PARAMETER SearchRoot
        Only scan objects under this OU.
     
    .PARAMETER Server
        The server/domain to connect to for the scan.
     
    .PARAMETER Credential
        The credentials to use for scanning.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Invoke-AhsCheck -ConfigFile C:\Scripts\adobjecthealth.config.psd1
         
        Executes the configuration defined / provided.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Config')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Config')]
        [hashtable]
        $Configuration,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [PSFFile]
        $ConfigFile,

        [Parameter(Mandatory = $true, ParameterSetName = 'All')]
        [switch]
        $All,

        [PsfArgumentCompleter('ADObjectHealthScan.Check.Name')]
        [string[]]
        $IncludeCheck,

        [PsfArgumentCompleter('ADObjectHealthScan.Check.Name')]
        [string[]]
        $ExcludeCheck,

        [PsfArgumentCompleter('ADObjectHealthScan.Check.Class')]
        [string[]]
        $IncludeClass,

        [PsfArgumentCompleter('ADObjectHealthScan.Check.Class')]
        [string[]]
        $ExcludeClass,

        [string]
        $SearchRoot,

        [string]
        $Server,

        [PSCredential]
        $Credential,

        [switch]
        $EnableException
    )
    begin {
        $adParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential, SearchRoot
        $config = $Configuration
        if ($ConfigFile) { $config = Import-PSFPowerShellDataFile -LiteralPath $ConfigFile | Microsoft.PowerShell.Utility\Select-Object -First 1 }
        if ($All) { $config = @{ _All = $true } }

        if (-not $config) { $config = @{} }

        $allClasses = (Get-AhsCheck).ObjectClass | Sort-Object -Unique
        if (-not $config._ObjectClasses) { $config._ObjectClasses = $allClasses }
        if ($IncludeClass) { $config._ObjectClasses = $config._ObjectClasses | Where-Object { $_ -in $IncludeClass } }
        if ($ExcludeClass) { $config._ObjectClasses = $config._ObjectClasses | Where-Object { $_ -notin $ExcludeClass } }

        if (-not $SearchRoot -and $config._SearchRoot) {
            $adParam.SearchRoot = $config._SearchRoot
        }
    }
    process {
        # Resolve Checks
        if ($config._All) { $checks = $script:ScanExtensions.Values }
        else {
            $checks = foreach ($checkName in $config.Keys) {
                if ($checkName -match '^_') { continue }
                if (-not $script:ScanExtensions[$checkName]) {
                    Write-PSFMessage -Level Warning -String 'Invoke-AhsCheck.Error.CheckNotFound' -StringValues $checkName
                    continue
                }
    
                $script:ScanExtensions[$checkName]
            }
        }
        $checks = $checks | Where-Object {
            (-not $IncludeCheck -or $_.Name -in $IncludeCheck) -and
            (-not $ExcludeCheck -or $_.Name -notin $ExcludeCheck)
        }

        # Resolve Check Configuration
        $effectiveConfig = @{ }
        foreach ($check in $checks) {
            $effectiveConfig[$check.Name] = $check.Parameters.Clone()
            foreach ($parameter in $config.$($check.Name).Keys) {
                $effectiveConfig[$check.Name][$parameter] = $config.$($check.Name).$parameter
            }
        }

        #region Perform all Scanning
        $objectClasses = $checks.ObjectClass | Sort-Object -Unique | Where-Object { $_ -in $config._ObjectClasses }
        foreach ($objectClass in $objectClasses) {
            $classChecks = $checks | Where-Object ObjectClass -Contains $objectClass
            # Calculate LDAP Filter
            $filterSegments = foreach ($check in $classChecks) {
                & $check.LdapFilter $effectiveConfig[$check.Name]
            }
            $ldapFilter = '(&(objectCategory={0})(|{1}))' -f $objectClass, ($filterSegments -join '')

            # Calculate Properties
            $properties = @('samAccountName', 'distinguishedName', 'userAccountControl') + $($classChecks.Properties) | Remove-PSFNull | Sort-Object -Unique

            # Collect Objects
            Write-PSFMessage -String 'Invoke-AhsCheck.Query.Send' -StringValues $objectClass, $ldapFilter
            $adObjects = Get-LdapObject @adParam -LdapFilter $ldapFilter -Property $properties

            # For Each object, generate findings
            foreach ($adObject in $adObjects) {
                foreach ($check in $classChecks) {
                    try { & $check.Check $adObject $effectiveConfig[$check.Name] $adParam }
                    catch { Write-PSFMessage -Level Error -String 'Invoke-AhsCheck.Error.CheckFailed' -StringValues $check.Name, $adObject.DistinguishedName -ErrorRecord $_ -PSCmdlet $PSCmdlet -EnableException $EnableException.ToBool() }
                }
            }
        }
        #endregion Perform all Scanning
    }
}