Files
SGU-CredentialProvider/scripts/Test-SguClientEnrollment.ps1
T

305 lines
12 KiB
PowerShell

[CmdletBinding()]
param(
[switch]$RequireDomainJoined,
[switch]$RequireRemoteAccess,
[switch]$RequireBrokerHealth,
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
[switch]$RequireRustDesk,
[string]$RustDeskServerAddress,
[switch]$Enforce
)
$ErrorActionPreference = 'Stop'
$providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
$passwordProviderClassId = '{60B78E88-EAD8-445C-9CFD-0B87F74EA6CD}'
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
$passwordProviderRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$passwordProviderClassId"
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
$settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json'
$issues = [Collections.Generic.List[string]]::new()
$standardLocalUserName = 'alumno'
$passwordNeverExpiresFlag = 0x10000
function Get-LocalGroupMemberSid {
param([Parameter(Mandatory)][string]$Name)
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$Name,group")
foreach ($member in @($group.psbase.Invoke('Members'))) {
try {
$sidBytes = $member.GetType().InvokeMember(
'objectSid',
[Reflection.BindingFlags]::GetProperty,
$null,
$member,
$null)
if ($sidBytes) {
([Security.Principal.SecurityIdentifier]::new([byte[]]$sidBytes, 0)).Value
}
}
catch {
# Keep validating known members when an old forest SID no longer resolves.
}
}
}
$computer = Get-CimInstance Win32_ComputerSystem
if ($RequireDomainJoined -and -not $computer.PartOfDomain) {
$issues.Add('The computer is not joined to a domain.')
}
$providerRegistered = Test-Path -LiteralPath $providerRegistryPath
if (-not $providerRegistered) {
$issues.Add('The SGU Credential Provider registration is missing.')
}
$registeredDll = $null
if (Test-Path -LiteralPath $classRegistryPath) {
$registeredDll = (Get-Item -LiteralPath $classRegistryPath).GetValue('')
}
$providerBinaryPresent = $registeredDll -and (Test-Path -LiteralPath $registeredDll -PathType Leaf)
if (-not $providerBinaryPresent) {
$issues.Add('The registered SGU COM binary is missing.')
}
$configuredDefault = $null
try {
$configuredDefault = Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath `
-Name DefaultCredentialProvider `
-ErrorAction Stop
}
catch {
# Report the missing or unreadable policy as a failed enrollment check.
}
$defaultProviderConfigured = $configuredDefault -eq $providerClassId
if (-not $defaultProviderConfigured) {
$issues.Add('The SGU provider is not assigned as the machine default credential provider.')
}
$lastSignedInUserHidden = $false
try {
$lastSignedInUserHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName `
-ErrorAction Stop) -eq 1
}
catch {
# Report the missing or unreadable policy as a failed enrollment check.
}
if (-not $lastSignedInUserHidden) {
$issues.Add('The last signed-in user is not hidden from LogonUI.')
}
$localUserEnumerationDisabled = $false
try {
$localUserEnumerationDisabled = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath `
-Name EnumerateLocalUsers `
-ErrorAction Stop) -eq 0
}
catch {
# Report the missing or unreadable policy as a failed enrollment check.
}
if (-not $localUserEnumerationDisabled) {
$issues.Add('Local user enumeration is not explicitly disabled for the domain client.')
}
$passwordProviderPreserved = Test-Path -LiteralPath $passwordProviderRegistryPath
if (-not $passwordProviderPreserved) {
$issues.Add('The built-in Microsoft password provider registration is missing.')
}
$standardLocalUser = Get-LocalUser -Name $standardLocalUserName -ErrorAction SilentlyContinue
$standardLocalUserPresent = [bool]$standardLocalUser
$standardLocalUserEnabled = $standardLocalUserPresent -and $standardLocalUser.Enabled
$standardLocalUserIsAdministrator = $false
$standardLocalUserInUsersGroup = $false
$standardLocalUserPasswordNeverExpires = $false
if ($standardLocalUserPresent) {
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
$usersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-545')
$administratorsGroup = Get-LocalGroup -SID $administratorsSid -ErrorAction Stop
$usersGroup = Get-LocalGroup -SID $usersSid -ErrorAction Stop
$administratorMembers = @(Get-LocalGroupMemberSid -Name $administratorsGroup.Name)
$standardMembers = @(Get-LocalGroupMemberSid -Name $usersGroup.Name)
$standardLocalUserIsAdministrator =
$administratorMembers -contains $standardLocalUser.SID.Value
$standardLocalUserInUsersGroup =
$standardMembers -contains $standardLocalUser.SID.Value
try {
$directoryEntry = [ADSI]("WinNT://$env:COMPUTERNAME/$standardLocalUserName,user")
$userFlags = [int]$directoryEntry.InvokeGet('UserFlags')
$standardLocalUserPasswordNeverExpires =
($userFlags -band $passwordNeverExpiresFlag) -ne 0
}
catch {
# Report the account as invalid when Windows cannot read its flags.
$standardLocalUserPasswordNeverExpires = $false
}
}
if (-not $standardLocalUserPresent) {
$issues.Add("The required standard local user '$standardLocalUserName' is missing.")
}
elseif (-not $standardLocalUserEnabled) {
$issues.Add("The required standard local user '$standardLocalUserName' is disabled.")
}
elseif ($standardLocalUserIsAdministrator) {
$issues.Add("The required standard local user '$standardLocalUserName' belongs to the local Administrators group.")
}
elseif (-not $standardLocalUserInUsersGroup) {
$issues.Add("The required standard local user '$standardLocalUserName' does not belong to the local Users group.")
}
elseif (-not $standardLocalUserPasswordNeverExpires) {
$issues.Add("The required standard local user '$standardLocalUserName' password is not configured to never expire.")
}
$settings = $null
try {
$settings = Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json
}
catch {
$issues.Add('The SGU provider settings file is missing or invalid.')
}
$clientCertificatePresent = $false
$serverCertificateTrusted = $false
$brokerHealth = $null
if ($settings) {
$clientCertificate = Get-ChildItem Cert:\LocalMachine\My |
Where-Object Thumbprint -eq $settings.ClientCertificateThumbprint |
Select-Object -First 1
$clientCertificatePresent = $clientCertificate -and $clientCertificate.HasPrivateKey
if (-not $clientCertificatePresent) {
$issues.Add('The client mTLS certificate with private key is missing.')
}
$serverCertificate = Get-ChildItem Cert:\LocalMachine\Root,Cert:\LocalMachine\CA |
Where-Object Thumbprint -eq $settings.ServerCertificateThumbprint |
Select-Object -First 1
$serverCertificateTrusted = [bool]$serverCertificate
if (-not $serverCertificateTrusted) {
$issues.Add('The broker certificate is not trusted by LocalMachine.')
}
if ($RequireBrokerHealth -and $clientCertificatePresent) {
try {
$healthUri = ([Uri]$settings.BrokerEndpoint).GetLeftPart([UriPartial]::Authority) + '/health/live'
$health = Invoke-RestMethod -Uri $healthUri -Certificate $clientCertificate -TimeoutSec 10
$brokerHealth = [string]$health.status
if ($brokerHealth -ne 'ok') {
$issues.Add('The broker health endpoint did not return ok.')
}
}
catch {
$issues.Add("The broker health check failed: $($_.Exception.Message)")
}
}
}
$dotNetRuntimePresent = $false
$dotnetCandidates = @(
(Get-Command dotnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue),
(Join-Path $env:ProgramFiles 'dotnet\dotnet.exe')
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) } | Select-Object -Unique
foreach ($dotnet in $dotnetCandidates) {
if (& $dotnet --list-runtimes | Select-String '^Microsoft\.NETCore\.App 10\.') {
$dotNetRuntimePresent = $true
break
}
}
if (-not $dotNetRuntimePresent) {
$issues.Add('The Microsoft .NET 10 x64 runtime is missing.')
}
$remoteAccessReady = $null
if ($RequireRemoteAccess) {
$remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-555')
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
$rdpMembers = @(Get-LocalGroupMemberSid -Name $remoteDesktopUsersGroup)
$remoteDesktopPrincipalSid = $null
try {
$remoteDesktopPrincipalSid = ([Security.Principal.NTAccount]::new($RemoteDesktopPrincipal)).Translate(
[Security.Principal.SecurityIdentifier]).Value
}
catch { }
$remoteAccessReady =
(Get-Service TermService).Status -eq 'Running' -and
(Get-Service WinRM).Status -eq 'Running' -and
(Get-ItemPropertyValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections) -eq 0 -and
$remoteDesktopPrincipalSid -and $rdpMembers -contains $remoteDesktopPrincipalSid
if (-not $remoteAccessReady) {
$issues.Add('RDP/WinRM or the authorized domain group is not fully configured.')
}
}
$rustDeskReady = $null
$rustDeskId = $null
if ($RequireRustDesk) {
if ([string]::IsNullOrWhiteSpace($RustDeskServerAddress)) {
$issues.Add('RustDesk validation requires RustDeskServerAddress.')
}
$rustDeskService = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue
$rustDeskStatePath = Join-Path $env:ProgramData 'SGU\RustDesk\Client\device.json'
$rustDeskSecretPath = Join-Path $env:ProgramData 'SGU\RustDesk\Client\access.secret'
$rustDeskConfigPath = Join-Path $env:WINDIR `
'ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml'
$rustDeskConfig = if (Test-Path -LiteralPath $rustDeskConfigPath -PathType Leaf) {
Get-Content -LiteralPath $rustDeskConfigPath -Raw
}
else {
''
}
$rustDeskState = $null
try {
$rustDeskState = Get-Content -LiteralPath $rustDeskStatePath -Raw | ConvertFrom-Json
$rustDeskId = [string]$rustDeskState.RustDeskId
}
catch {
# The checks below report the missing or invalid state as one enrollment issue.
}
$rustDeskReady =
$rustDeskService -and $rustDeskService.Status -eq 'Running' -and
(Test-Path -LiteralPath $rustDeskSecretPath -PathType Leaf) -and
$rustDeskState -and $rustDeskState.ServerAddress -eq $RustDeskServerAddress -and
$rustDeskId -match '^\d+$' -and
$rustDeskConfig -match [regex]::Escape("rendezvous_server = '$RustDeskServerAddress`:21116'")
if (-not $rustDeskReady) {
$issues.Add('RustDesk is not installed, running, or configured for the expected self-hosted server.')
}
}
$result = [pscustomobject]@{
ComputerName = $env:COMPUTERNAME
Domain = $computer.Domain
DomainJoined = [bool]$computer.PartOfDomain
ProviderRegistered = $providerRegistered
ProviderBinary = $registeredDll
ProviderBinaryPresent = [bool]$providerBinaryPresent
DefaultProviderConfigured = $defaultProviderConfigured
LastSignedInUserHidden = $lastSignedInUserHidden
LocalUserEnumerationDisabled = $localUserEnumerationDisabled
PasswordProviderPreserved = $passwordProviderPreserved
StandardLocalUserPresent = $standardLocalUserPresent
StandardLocalUserEnabled = $standardLocalUserEnabled
StandardLocalUserIsAdministrator = $standardLocalUserIsAdministrator
StandardLocalUserInUsersGroup = $standardLocalUserInUsersGroup
StandardLocalUserPasswordNeverExpires = $standardLocalUserPasswordNeverExpires
SettingsPresent = [bool]$settings
ClientCertificatePresent = [bool]$clientCertificatePresent
ServerCertificateTrusted = $serverCertificateTrusted
DotNetRuntimePresent = $dotNetRuntimePresent
BrokerHealth = $brokerHealth
RemoteAccessReady = $remoteAccessReady
RustDeskReady = $rustDeskReady
RustDeskId = $rustDeskId
IsValid = $issues.Count -eq 0
Issues = $issues.ToArray()
}
$result
if ($Enforce -and -not $result.IsValid) {
throw "SGU client enrollment is invalid: $($issues -join ' ')"
}