src/collect/Get-ScoutHybridIdentityLocalEvidence.ps1

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Get-ScoutHybridIdentityLocalEvidence {
    [CmdletBinding()]
    param(
        [Parameter(DontShow)]
        [AllowNull()]
        [System.Collections.IDictionary]$CommandAvailability
    )

    $resources=[System.Collections.Generic.List[object]]::new()
    $sourceOperations=[System.Collections.Generic.List[object]]::new()
    $collectionHealth=[System.Collections.Generic.List[object]]::new()

    function Add-HybridLocalRows {
        param([string]$Dataset,[string]$Command,[object[]]$Rows,[datetime]$StartedAt)
        $collectedAt=Get-Date
        $index=0
        foreach($row in @($Rows|Where-Object{$null-ne $_})){
            $index++
            $nameProperty=@($row.PSObject.Properties|Where-Object Name -in @('Name','Forest','Target')|Select-Object -First 1)
            $name=if($nameProperty.Count){[string]$nameProperty[0].Value}else{"$Dataset-$index"}
            $resources.Add([pscustomobject][ordered]@{
                    id="/providers/AzureScout/hybridIdentity/$Dataset/$([uri]::EscapeDataString($name))-$index"
                    name=$name;type="AZSC/HybridIdentity/$Dataset"
                    properties=[pscustomobject][ordered]@{Raw=$row}
                    AZSC=[pscustomobject][ordered]@{Source='Local read-only identity command';Dataset=$Dataset;Operation=$Command;Host=$env:COMPUTERNAME;CollectedAt=$collectedAt.ToString('o')}
                })
        }
        $sourceOperations.Add([pscustomobject][ordered]@{
                Source='Local read-only identity command';Dataset=$Dataset;Operation=$Command;Host=$env:COMPUTERNAME
                Status=if(@($Rows).Count){'Success'}else{'Empty'};Count=@($Rows).Count;Reason=$null
                StartedAt=$StartedAt.ToString('o');CompletedAt=$collectedAt.ToString('o')
            })
    }

    function Invoke-HybridLocalRead {
        param([string]$Dataset,[string]$Command,[scriptblock]$Operation,[string]$MissingReason)
        $startedAt=Get-Date
        $commandIsAvailable = if ($null -ne $CommandAvailability -and $CommandAvailability.Contains($Command)) {
            [bool]$CommandAvailability[$Command]
        }
        else {
            $null -ne (Get-Command $Command -ErrorAction SilentlyContinue)
        }
        if(-not $commandIsAvailable){
            $reason=$MissingReason
            $sourceOperations.Add([pscustomobject]@{Source='Local read-only identity command';Dataset=$Dataset;Operation=$Command;Host=$env:COMPUTERNAME;Status='NotAssessed';Count=0;Reason=$reason;StartedAt=$startedAt.ToString('o');CompletedAt=(Get-Date).ToString('o')})
            $collectionHealth.Add([pscustomobject]@{Dataset="HybridIdentity/$Dataset";Operation=$Command;Status='NotAssessed';Reason=$reason;ResourceTypes=@("AZSC/HybridIdentity/$Dataset")})
            return
        }
        try{
            $rows=@(& $Operation)
            Add-HybridLocalRows -Dataset $Dataset -Command $Command -Rows $rows -StartedAt $startedAt
        }catch{
            $reason=$_.Exception.Message
            $sourceOperations.Add([pscustomobject]@{Source='Local read-only identity command';Dataset=$Dataset;Operation=$Command;Host=$env:COMPUTERNAME;Status='Unavailable';Count=0;Reason=$reason;StartedAt=$startedAt.ToString('o');CompletedAt=(Get-Date).ToString('o')})
            $collectionHealth.Add([pscustomobject]@{Dataset="HybridIdentity/$Dataset";Operation=$Command;Status='Unavailable';Reason=$reason;ResourceTypes=@("AZSC/HybridIdentity/$Dataset")})
        }
    }

    Invoke-HybridLocalRead -Dataset 'EntraConnectScheduler' -Command 'Get-ADSyncScheduler' -MissingReason 'Microsoft Entra Connect ADSync cmdlets are not installed on this host.' -Operation {Get-ADSyncScheduler}
    Invoke-HybridLocalRead -Dataset 'EntraConnectConnectors' -Command 'Get-ADSyncConnector' -MissingReason 'Microsoft Entra Connect ADSync cmdlets are not installed on this host.' -Operation {Get-ADSyncConnector}
    Invoke-HybridLocalRead -Dataset 'EntraConnectGlobalSettings' -Command 'Get-ADSyncGlobalSettings' -MissingReason 'Microsoft Entra Connect ADSync cmdlets are not installed on this host.' -Operation {Get-ADSyncGlobalSettings}
    Invoke-HybridLocalRead -Dataset 'EntraConnectService' -Command 'Get-CimInstance' -MissingReason 'CIM is unavailable, so the local ADSync service version and binary path were not assessed.' -Operation {Get-CimInstance -ClassName Win32_Service -Filter "Name='ADSync'"}
    Invoke-HybridLocalRead -Dataset 'EntraConnectHealthAgents' -Command 'Get-Service' -MissingReason 'Service Control Manager access is unavailable, so Entra Connect Health agents were not assessed.' -Operation {Get-Service -Name 'AzureADConnectHealth*' -ErrorAction SilentlyContinue}
    Invoke-HybridLocalRead -Dataset 'ActiveDirectoryForest' -Command 'Get-ADForest' -MissingReason 'ActiveDirectory cmdlets are not installed or this host is not joined to a readable forest.' -Operation {Get-ADForest}
    Invoke-HybridLocalRead -Dataset 'ActiveDirectoryDomain' -Command 'Get-ADDomain' -MissingReason 'ActiveDirectory cmdlets are not installed or this host is not joined to a readable domain.' -Operation {Get-ADDomain}
    Invoke-HybridLocalRead -Dataset 'ActiveDirectoryTrusts' -Command 'Get-ADTrust' -MissingReason 'ActiveDirectory cmdlets are not installed or this host is not joined to a readable domain.' -Operation {Get-ADTrust -Filter *}

    [pscustomobject]@{Resources=@($resources);SourceOperations=@($sourceOperations);CollectionHealth=@($collectionHealth)}
}