Tools/Grant-ADEHMWmiPermission.ps1

#requires -Version 5.1
<#
.SYNOPSIS
    Grants the ADEHM service account/group the minimal WMI rights
    (Enable Account + Remote Enable) on the required namespaces of each
    domain controller.
 
.DESCRIPTION
    Run ONCE, as a domain administrator, from a Windows PowerShell 5.1
    console.
 
    Idempotent: an already-compliant entry returns 'Already configured'.
 
    Note: WMI-Activity logs may show denials on root\interop during CIM
    sessions. Field testing shows these are benign, non-blocking probes:
    no root\interop grant is required (least privilege). Only root/cimv2
    is granted by default.
 
.PARAMETER Account
    Account or group in DOMAIN\name format (e.g. DOMAIN\GRP-AD-Monitoring).
 
.PARAMETER DomainControllers
    DCs to configure. When omitted, read through -ConfigPath.
 
.PARAMETER ConfigPath
    Path to ADEHM.config.psd1 to extract the DC list.
 
.PARAMETER Namespaces
    WMI namespaces to process. Default: root/cimv2.
 
.PARAMETER Remove
    Removes the account/group entries instead of adding them (rollback).
 
.EXAMPLE
    .\Grant-ADEHMWmiPermission.ps1 -Account 'DOMAIN\GRP-AD-Monitoring' -ConfigPath ..\Config\ADEHM.config.psd1
#>


[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory)]
    [ValidatePattern('^[^\\]+\\[^\\]+$')]
    [string]$Account,

    [string[]]$DomainControllers,

    [string]$ConfigPath,

    [string[]]$Namespaces = @('root/cimv2'),

    [switch]$Remove
)

# --- DC list resolution ------------------------------------------------
if (-not $DomainControllers) {
    if (-not $ConfigPath) {
        throw 'Provide -DomainControllers or -ConfigPath (ADEHM.config.psd1 file).'
    }
    $config = Import-PowerShellDataFile -Path $ConfigPath
    $DomainControllers = $config.DomainControllers
}

if (-not $DomainControllers) {
    throw 'No domain controller to configure.'
}

# --- Remote block executed on each DC -----------------------------------
$remoteBlock = {
    param($Account, $Remove, $Namespaces)

    $WBEM_ENABLE        = 0x1     # Enable Account
    $WBEM_REMOTE_ACCESS = 0x20    # Remote Enable
    $targetMask         = $WBEM_ENABLE -bor $WBEM_REMOTE_ACCESS   # 0x21

    # Resolve the account/group SID
    $ntAccount = New-Object System.Security.Principal.NTAccount($Account)
    try {
        $sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
    }
    catch {
        throw "Account '$Account' not found from $env:COMPUTERNAME : $($_.Exception.Message)"
    }

    $statuses = foreach ($ns in $Namespaces) {

        # The reliable invocation method is Invoke-WmiMethod on the
        # '__SystemSecurity=@' singleton - calling the method directly on
        # the object returned by Get-WmiObject fails on some systems.
        $wmiParams = @{ Namespace = $ns; Path = '__SystemSecurity=@' }

        $getResult = Invoke-WmiMethod @wmiParams -Name 'GetSecurityDescriptor'
        if ($getResult.ReturnValue -ne 0) {
            "$ns : GetSecurityDescriptor failed (code $($getResult.ReturnValue))"
            continue
        }
        $sd = $getResult.Descriptor

        $existing = @($sd.DACL) | Where-Object { $_.Trustee.SIDString -eq $sid.Value }

        if ($Remove) {
            if (-not $existing) { "$ns : no entry to remove"; continue }
            $sd.DACL = @(@($sd.DACL) | Where-Object { $_.Trustee.SIDString -ne $sid.Value })
            $setResult = Invoke-WmiMethod @wmiParams -Name 'SetSecurityDescriptor' -ArgumentList $sd.PSObject.ImmediateBaseObject
            if ($setResult.ReturnValue -ne 0) { "$ns : SetSecurityDescriptor failed (code $($setResult.ReturnValue))"; continue }
            "$ns : entry removed"
            continue
        }

        if ($existing) {
            $hasRights = $existing | Where-Object { ($_.AccessMask -band $targetMask) -eq $targetMask }
            if ($hasRights) { "$ns : already configured"; continue }
            # Entry present but incomplete rights: removed for clean recreation
            $sd.DACL = @(@($sd.DACL) | Where-Object { $_.Trustee.SIDString -ne $sid.Value })
        }

        # New ACE (Allow, this namespace only)
        $trustee = (New-Object System.Management.ManagementClass('root/cimv2:Win32_Trustee')).CreateInstance()
        $trustee.SIDString = $sid.Value

        $ace = (New-Object System.Management.ManagementClass('root/cimv2:Win32_ACE')).CreateInstance()
        $ace.AccessMask = $targetMask
        $ace.AceType    = 0        # ACCESS_ALLOWED
        $ace.AceFlags   = 0        # no inheritance to sub-namespaces
        $ace.Trustee    = $trustee

        $sd.DACL += $ace.PSObject.ImmediateBaseObject

        $setResult = Invoke-WmiMethod @wmiParams -Name 'SetSecurityDescriptor' -ArgumentList $sd.PSObject.ImmediateBaseObject
        if ($setResult.ReturnValue -ne 0) { "$ns : SetSecurityDescriptor failed (code $($setResult.ReturnValue))"; continue }
        "$ns : rights granted (Enable Account + Remote Enable)"
    }

    return ($statuses -join ' | ')
}

# --- Apply to each DC -----------------------------------------------------
$isRemove = [bool]$Remove   # explicit conversion BEFORE the call: an inline
                            # cast in -ArgumentList would be parsed as text

$results = foreach ($dc in $DomainControllers) {
    $action = if ($isRemove) { "Remove WMI rights of $Account ($($Namespaces -join ', '))" }
              else           { "Grant WMI rights to $Account ($($Namespaces -join ', '))" }
    if (-not $PSCmdlet.ShouldProcess($dc, $action)) { continue }

    try {
        $status = Invoke-Command -ComputerName $dc -ScriptBlock $remoteBlock -ArgumentList $Account, $isRemove, $Namespaces -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
    }
    Write-Warning "$($failed.Count) DC(s) failed. See details above."
    exit 1
}

Write-Host "`nDone. Restart WinRM on the DCs (Restart-Service WinRM) before retesting: stale WinRM host processes serve requests with pre-change access state." -ForegroundColor Green