Tools/Grant-ADEHMServiceReadPermission.ps1
|
#requires -Version 5.1 <# .SYNOPSIS Grants the ADEHM group/account READ-ONLY access to the monitored Windows services on each domain controller. .DESCRIPTION On a hardened system, permission to enumerate services (Service Control Manager SDDL) is not enough: each service then filters callers through its own security descriptor. A service whose SD does not grant read to the caller becomes INVISIBLE in WMI results (query accepted, empty result). This script adds the standard read ACE (A;;CCLCSWLOCRRC;;;<SID>) - the same right set granted by default to interactive users: configuration and status query only, NO start/stop/modify rights. Idempotent; each service's previous SDDL is backed up under C:\Windows\Temp\ on the DC before modification. .PARAMETER Account Account or group in DOMAIN\name format (e.g. DOMAIN\GRP-AD-Monitoring). .PARAMETER Services Services to process. When omitted and -ConfigPath is provided, the list is read from the Services section of the ADEHM configuration. .EXAMPLE .\Grant-ADEHMServiceReadPermission.ps1 -Account 'DOMAIN\GRP-AD-Monitoring' -ConfigPath ..\Config\ADEHM.config.psd1 #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [ValidatePattern('^[^\\]+\\[^\\]+$')] [string]$Account, [string[]]$DomainControllers, [string]$ConfigPath, [string[]]$Services ) # --- DC and service list resolution from configuration ------------------- if (-not $DomainControllers -or -not $Services) { if (-not $ConfigPath) { throw 'Provide -ConfigPath, or both -DomainControllers and -Services.' } $config = Import-PowerShellDataFile -Path $ConfigPath if (-not $DomainControllers) { $DomainControllers = $config.DomainControllers } if (-not $Services) { $Services = $config.Services | ForEach-Object { $_.Name } } } # --- SID resolution ------------------------------------------------------- $sid = (New-Object System.Security.Principal.NTAccount($Account)).Translate([System.Security.Principal.SecurityIdentifier]).Value # --- Remote block --------------------------------------------------------- $remoteBlock = { param($sid, $services) $ace = "(A;;CCLCSWLOCRRC;;;$sid)" # read only (same set as the Interactive Users default) $statuses = foreach ($svc in $services) { $raw = & sc.exe sdshow $svc 2>&1 if ($LASTEXITCODE -ne 0) { "$svc : cannot read SDDL ($(($raw | Select-Object -First 1)))" continue } $current = ($raw | Where-Object { $_ -match '\S' }) -join '' if ($current -match [regex]::Escape(";;;$sid)")) { "$svc : already present" continue } $backup = "C:\Windows\Temp\svc_sddl_{0}_{1}.txt" -f $svc, (Get-Date -Format 'yyyyMMdd_HHmmss') Set-Content -Path $backup -Value $current # Insert at end of DACL, before the SACL when present $idx = $current.IndexOf('S:') $new = if ($idx -ge 0) { $current.Insert($idx, $ace) } else { $current + $ace } $out = & sc.exe sdset $svc $new 2>&1 if ($LASTEXITCODE -ne 0) { "$svc : sdset failed ($out)" } else { "$svc : read granted" } } return ($statuses -join ' | ') } # --- Apply ---------------------------------------------------------------- $results = foreach ($dc in $DomainControllers) { if (-not $PSCmdlet.ShouldProcess($dc, "Grant service read ($($Services -join ', ')) to $Account")) { continue } try { $status = Invoke-Command -ComputerName $dc -ScriptBlock $remoteBlock -ArgumentList $sid, $Services -ErrorAction Stop [PSCustomObject]@{ DC = $dc; Status = $status; Error = $null } } catch { [PSCustomObject]@{ DC = $dc; Status = 'FAILED'; Error = $_.Exception.Message } } } $results | Format-Table -AutoSize -Wrap $failed = @($results | Where-Object { $_.Status -eq 'FAILED' }) if ($failed.Count -gt 0) { Write-Host "`nFailure details:" -ForegroundColor Yellow foreach ($f in $failed) { Write-Host (" {0}`n {1}`n" -f $f.DC, $f.Error) -ForegroundColor Yellow } exit 1 } Write-Host "`nDone. Restart WinRM on the DCs (Restart-Service WinRM) before retesting." -ForegroundColor Green |