modules/RDS/RDS.psm1

<#
    RDS.psm1 - Remote Desktop Services discovery (read-only). Role-gated.
#>


function Get-DiscoveryModuleMetadata {
    [pscustomobject]@{
        ModuleName='RDS'; DisplayName='Remote Desktop Services'; Category='Identity'; Version='1.0.0'
        DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$false; RequiresDomainContext=$false
        RequiresRole='RDS-RD-Server'; EstimatedImpact='Low'; CanRunAsSystem=$true
        ProducesDatasets=@('RdsDiscovery')
        ProducesRisks=$true; ProducesFollowUpQuestions=$true; SupportsDeepMode=$true; SupportsComplianceLens=$false
    }
}

function Test-RdsPresent {
    # The Terminal Server registry keys, the licensing key and TermService exist on EVERY Windows
    # Server (plain RDP administration), so their mere presence says nothing. A file server was
    # reported as an RDS host on that basis. Require evidence of actual RDS use instead.
    $mode = Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\RCM\Licensing Core' -Name 'LicensingMode'
    if ($mode -in 2, 4) { return $true }                      # Per Device / Per User CAL mode was actually chosen
    if (Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\TermService\Parameters\LicenseServers' -Name 'SpecifiedLicenseServers') { return $true }
    try { if (Get-CommandAvailable -Name 'Get-RDServer') { if (@(Get-RDServer -ErrorAction SilentlyContinue).Count -gt 0) { return $true } } } catch { }
    return $false
}

function Test-DiscoveryPrerequisites {
    param([object]$Context)
    $rolePresent = $false
    if ($Context.DataSets.Contains('RolesFeatures')) { $rolePresent = (@($Context.DataSets['RolesFeatures'].Rows | Where-Object { $_.Name -match '(?i)^RDS-' -and $_.InstallState -match '(?i)Installed' }).Count -gt 0) }
    if ($rolePresent -or (Test-RdsPresent)) { return [pscustomobject]@{ ModuleName='RDS'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() } }
    [pscustomobject]@{ ModuleName='RDS'; CanRun=$false; Status='NotApplicable'; Reason='RDS roles not detected.'; Limitations=@() }
}

function Invoke-DiscoveryCollection {
    param([object]$Context)
    $rows=[System.Collections.Generic.List[object]]::new()
    # Licensing mode/server (read-only registry)
    try {
        $lic = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\RCM\Licensing Core'
        $mode = Get-RegistryValueSafe -Path $lic -Name 'LicensingMode'
        # Modes 1 (Remote Administration, the default on every server) and 5 (not configured) are not RDS licensing.
        if ($mode -in 2, 4) { $modeName = switch ($mode) { 2 {'Per Device'} 4 {'Per User'} }; $rows.Add([pscustomobject]@{ Item='RDS Licensing'; Type='Licensing'; Detail=$modeName }) }
        $ls = Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\TermService\Parameters\LicenseServers' -Name 'SpecifiedLicenseServers'
        if ($ls) { $rows.Add([pscustomobject]@{ Item='RDS License Servers'; Type='Licensing'; Detail=($ls -join ', ') }) }
    } catch { }
    # Role services from RolesFeatures
    try {
        if ($Context.DataSets.Contains('RolesFeatures')) {
            foreach ($r in @($Context.DataSets['RolesFeatures'].Rows | Where-Object { $_.Name -match '(?i)^RDS-' -and $_.InstallState -match '(?i)Installed' })) {
                $rows.Add([pscustomobject]@{ Item=$r.DisplayName; Type='RoleService'; Detail=$r.Name })
            }
        }
    } catch { }
    # FSLogix / UPD indicators
    try { if (Get-Service -Name 'frxsvc' -ErrorAction SilentlyContinue) { $rows.Add([pscustomobject]@{ Item='FSLogix'; Type='ProfileContainer'; Detail='FSLogix service present' }) } } catch { }
    # Collections (best-effort)
    try { if (Get-CommandAvailable -Name 'Get-RDSessionCollection') { foreach ($c in (Get-RDSessionCollection -ErrorAction SilentlyContinue)) { $rows.Add([pscustomobject]@{ Item=$c.CollectionName; Type='Collection'; Detail=$c.CollectionDescription }) } } } catch { }
    if ($rows.Count -eq 0) { $rows.Add([pscustomobject]@{ Item='RDS'; Type='Detected'; Detail='RDS indicators present; detailed configuration unavailable.' }) }
    return ,@{ RdsDiscovery=@($rows) }
}

function ConvertTo-DiscoveryDatasets {
    param([object]$Context, $RawData)
    $rows = @(if ($RawData -and $RawData.RdsDiscovery) { @($RawData.RdsDiscovery) } else { @() })
    Add-DataSet -Context $Context -Name 'RdsDiscovery' -Description 'Remote Desktop Services indicators.' -Rows $rows -Visibility 'Internal' -SourceModule 'RDS' | Out-Null
}

function Get-DiscoveryFollowUpQuestions {
    param([object]$Context)
    try { if ($Context.DataSets.Contains('RdsDiscovery') -and @($Context.DataSets['RdsDiscovery'].Rows).Count -gt 0) { Add-FollowUpQuestion -Context $Context -Category 'Critical systems' -Module 'RDS' -Audience 'Both' -Question 'How is RDS licensing configured (per-user/per-device, license server), and who validates user sessions and published apps after a change?' | Out-Null } } catch { }
}

Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets','Get-DiscoveryFollowUpQuestions','Test-RdsPresent'