sqlinsight-pro/db/permissions/grant-windows-server.ps1
Søren Eggert Lundsteen Olsen 697d243658 Review 1.5: grant-windows-server.ps1 backs up, removes exactly what it gave, and never more than read
The grant script changes SCM and WMI security on customer servers. Before, a re-run
replaced every ACE of the account on a WMI namespace (also a customer's Deny), -Remove
took the account out of groups and ACEs it had before, only the SCM SDDL was backed up
(and never used), Remote Management Users also opened PowerShell Remoting, and
-EnableFirewall narrowed Windows' built-in rules for every other tool and was never undone.

- Checks everything before changing anything: elevation, the account resolves (broad
  groups refused), Deny ACEs stop the script, -EnableFirewall needs -AgentAddress.
- Backs up the original SDDLs (SCM, WMI namespaces, WinRM), group memberships and
  firewall rules before the first change; records what it added in a state file in an
  administrators-only folder; -Remove removes only that.
- SDDL handled with RawSecurityDescriptor (GetSD/SetSD, sc sdshow/sdset): exactly one
  ACE added in canonical order, verified before writing and after; aliases (NS) work.
- No Remote Management Users: -WinRM adds GENERIC_READ (no Execute) to WinRM's RootSDDL;
  DCOM is the default.
- Firewall: its own "SQLInsight Agent: ..." rules only from the agent's address,
  removed again with -Remove; Windows' rules untouched.
- -WhatIf, groups by SID, Danish messages, PowerShell 2.0 syntax.
- ServerGrantScript.For does not fill in an account that could end the PowerShell
  string (typographic quotes, control characters); tests run the SDDL logic in
  Windows PowerShell against real default SDDLs.
- docs/windows-servere.md: rights, safety net, firewall.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:31:49 +02:00

467 lines
25 KiB
PowerShell

<#
SQLInsight Pro: least-privilege rettigheder til agentens konto på en Windows-server
=====================================================================================
Giver SQLInsight-agentens konto præcis den læseadgang, den skal bruge for at overvåge denne server:
CPU, hukommelse, diske, tjenester, hændelseslog og planlagte opgaver. Aldrig lokal administrator, og
aldrig ret til at køre kode på serveren (heller ikke PowerShell Remoting).
Scriptet ændrer kun adgangsrettigheder: det starter, stopper eller ændrer ingen tjenester, opgaver eller
indstillinger, og det læser ingen data.
Afprøv det først på en server, der ikke er i produktion.
Kør som administrator på den server, der skal overvåges (Windows PowerShell 2.0 eller nyere):
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -WhatIf # vis, hvad der ville ske; ændrer intet
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$'
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -WinRM -EnableFirewall -AgentAddress 10.0.0.15
Fjern præcis det, scriptet har givet, igen:
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -Remove
Agentens konto: kører agenten som NT SERVICE\SQLInsight Agent, LocalSystem eller NETWORK SERVICE, kommer den
til andre servere som computerkontoen DOMÆNE\COMPUTERNAVN$. En gMSA eller domænekonto er den samme overalt.
Hvad der gives, og hvorfor:
Performance Monitor Users ydelsestællere: CPU, hukommelse, paging
Event Log Readers hændelseslogge, der tillader gruppen (System, Application m.fl.; aldrig Security)
Distributed COM Users WMI/CIM over DCOM (RPC)
WMI: Enable Account + Remote Enable (0x21) på root\cimv2 og root\Microsoft\Windows\TaskScheduler, kun på
selve navnerummet (ikke arvet). Ingen skrive- eller metoderet ud over Windows'
standard.
Service Control Manager forbinde, liste tjenester, læse låsestatus og sikkerhed (CC LC RP RC) - det
samme, som en lokalt logget på bruger har. Ingen ret til at starte eller stoppe.
Kun med -WinRM læseret (GENERIC_READ, ingen Execute) i WinRM's RootSDDL, så WMI kan læses over
WinRM (TCP 5985). Kontoen kommer IKKE i Remote Management Users, for den gruppe
giver også PowerShell Remoting. Uden -WinRM bruger agenten DCOM.
Hvad der IKKE gives: lokal administrator, PowerShell Remoting/WinRS, ret til at starte/stoppe tjenester,
skrive i WMI, læse filer eller Security-loggen.
Firewall (kun med -EnableFirewall og -AgentAddress, Windows Server 2012 og nyere): opretter scriptets egne regler
"SQLInsight Agent: ..." for RPC (WMI over DCOM og hændelseslog) og med -WinRM for TCP 5985, kun fra agentens
adresse. Windows' indbyggede regler røres ikke.
Sikkerhedsnet:
- Intet ændres, før alt er læst og kontrolleret (konto, grupper, sikkerhed, firewall). Fejler kontrollen, er
intet ændret.
- Før første ændring gemmes de oprindelige indstillinger (SDDL for Service Control Manager, WMI-navnerummene og
WinRM, gruppemedlemskaber og firewallregler) i %ProgramData%\SQLInsight\grant-windows-server\<SID>-<tid>-foer.xml.
- Hvad scriptet selv har tilføjet, står i %ProgramData%\SQLInsight\grant-windows-server\<SID>.xml (kun
administratorer har adgang til mappen). -Remove fjerner kun det; intet, der fandtes før.
- Har kontoen allerede en rettighed, røres den ikke (og fjernes ikke med -Remove). Har den en "Nægt"-regel,
stopper scriptet uden at ændre noget.
- Hvert skridt kontrolleres bagefter. Fejler et skridt, står det, der allerede er gjort, i tilstandsfilen, så
scriptet kan køres igen eller med -Remove.
- Manuel gendannelse af Service Control Manager: sc.exe sdset scmanager "<SDDL fra -foer.xml>".
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string] $Account = 'DOMÆNE\AGENTSERVER$', # <== agentens konto
[switch] $Remove,
[switch] $WinRM,
[switch] $EnableFirewall,
[string] $AgentAddress
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$placeholder = 'DOMÆNE\AGENTSERVER$'
$whatIf = [bool] $WhatIfPreference
# ---- 1. Kontrol: intet ændres, før alt her er i orden ---------------------------------------------------------
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Kør scriptet i en PowerShell, der er startet som administrator ("Kør som administrator"). Intet er ændret.'
}
if ([string]::IsNullOrEmpty($Account) -or $Account -eq $placeholder) {
throw "Angiv agentens konto, fx -Account 'DOMÆNE\AGENTSERVER`$' (SQLInsight Web viser den under serveren). Intet er ændret."
}
foreach ($c in $Account.ToCharArray()) {
if ([char]::IsControl($c)) { throw 'Kontonavnet indeholder et kontroltegn. Intet er ændret.' }
}
try {
$sidObj = (New-Object Security.Principal.NTAccount($Account)).Translate([Security.Principal.SecurityIdentifier])
} catch {
throw "Kontoen '$Account' kunne ikke slås op (findes den, og kan serveren nå domænet?). Intet er ændret."
}
$sid = $sidObj.Value
if ($sid -eq 'S-1-5-18') {
Write-Host 'Agenten kører som LocalSystem på denne server og har allerede adgang. Intet at gøre.'
return
}
# Grupper med mange medlemmer: rettighederne skal gives til agentens egen konto, aldrig til alle.
$broad = @('S-1-1-0', 'S-1-5-7', 'S-1-5-2', 'S-1-5-4', 'S-1-5-11', 'S-1-5-32-544', 'S-1-5-32-545', 'S-1-5-32-546')
if (($broad -contains $sid) -or ($sid -match '^S-1-5-21-.+-(512|513|514|515|516|519)$')) {
throw "'$Account' er en bred gruppe. Giv rettighederne til agentens egen konto (gMSA, domænekonto eller computerkonto). Intet er ændret."
}
if ($EnableFirewall -and -not $Remove) {
if ([string]::IsNullOrEmpty($AgentAddress)) {
throw 'Med -EnableFirewall skal -AgentAddress angives (agentens IP-adresse eller net, fx 10.0.0.15 eller 10.0.0.0/24), så portene kun åbnes for agenten. Intet er ændret.'
}
foreach ($a in $AgentAddress.Split(',')) {
$ip = $null
if (-not [Net.IPAddress]::TryParse($a.Trim().Split('/')[0], [ref] $ip)) {
throw "-AgentAddress '$a' er ikke en IP-adresse eller et net (fx 10.0.0.15 eller 10.0.0.0/24). Intet er ændret."
}
}
if (-not (Get-Command New-NetFirewallRule -ErrorAction SilentlyContinue)) {
throw '-EnableFirewall kræver Windows Server 2012 eller nyere. Åbn portene manuelt (se docs/windows-servere.md). Intet er ændret.'
}
} elseif ($AgentAddress -and -not $Remove) {
Write-Host ' (-AgentAddress bruges kun sammen med -EnableFirewall)'
}
$stateDir = Join-Path $env:ProgramData 'SQLInsight\grant-windows-server'
$statePath = Join-Path $stateDir "$sid.xml"
if (Test-Path $statePath) {
$state = Import-Clixml $statePath
} else {
$state = @{ Sid = $sid; Account = $Account; Groups = @(); Wmi = @(); Scm = $false; WinRMRoot = $false; Firewall = @() }
}
if ($Remove) { $verb = 'Fjerner det, scriptet har givet' } else { $verb = 'Giver rettigheder til' }
Write-Host "$verb $Account ($sid) på $env:COMPUTERNAME"
if ($Remove -and -not (Test-Path $statePath)) {
Write-Host "Intet at fjerne: scriptet har ikke givet $Account noget på denne server ($statePath findes ikke)."
return
}
# ---- Hjælpefunktioner (sikkerhedsbeskrivelser som SDDL, ændret via .NET's RawSecurityDescriptor) ---------------
$accessAllowed = [Security.AccessControl.AceQualifier]::AccessAllowed
$accessDenied = [Security.AccessControl.AceQualifier]::AccessDenied
$allSections = [Security.AccessControl.AccessControlSections]::All
function Get-Aces([string] $Sddl) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
if ($raw.DiscretionaryAcl -eq $null) { return @() }
return @($raw.DiscretionaryAcl | Where-Object { $_ -is [Security.AccessControl.CommonAce] -and $_.SecurityIdentifier -eq $sidObj })
}
# Har kontoen en Nægt-regel? Så stopper scriptet: den er sat med vilje.
function Test-Denied([string] $Sddl) {
return @(Get-Aces $Sddl | Where-Object { $_.AceQualifier -eq $accessDenied }).Count -gt 0
}
# Har kontoen allerede (mindst) disse rettigheder fra en Tillad-regel?
function Test-Allowed([string] $Sddl, [int] $Mask) {
return @(Get-Aces $Sddl | Where-Object { $_.AceQualifier -eq $accessAllowed -and (($_.AccessMask -band $Mask) -eq $Mask) }).Count -gt 0
}
# Fjerner præcis én Tillad-regel for kontoen med netop denne maske, ikke arvet og uden flag (den, scriptet satte).
function Remove-Ace([string] $Sddl, [int] $Mask) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
if ($raw.DiscretionaryAcl -eq $null) { return $Sddl }
for ($i = 0; $i -lt $raw.DiscretionaryAcl.Count; $i++) {
$ace = $raw.DiscretionaryAcl[$i]
if ($ace -is [Security.AccessControl.CommonAce] -and $ace.SecurityIdentifier -eq $sidObj -and $ace.AceQualifier -eq $accessAllowed `
-and $ace.AccessMask -eq $Mask -and $ace.AceFlags -eq [Security.AccessControl.AceFlags]::None) {
$raw.DiscretionaryAcl.RemoveAce($i)
return $raw.GetSddlForm($allSections)
}
}
return $Sddl
}
# Indsætter præcis én Tillad-regel (ikke arvet) efter de eksplicitte regler og før de arvede (kanonisk orden).
function Add-Ace([string] $Sddl, [int] $Mask) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
if ($raw.DiscretionaryAcl -eq $null) { throw 'Sikkerhedsbeskrivelsen har ingen DACL; den ændres ikke.' }
$ace = New-Object Security.AccessControl.CommonAce -ArgumentList @([Security.AccessControl.AceFlags]::None, $accessAllowed, $Mask, $sidObj, $false, $null)
$at = 0
while ($at -lt $raw.DiscretionaryAcl.Count -and -not $raw.DiscretionaryAcl[$at].IsInherited) { $at++ }
$raw.DiscretionaryAcl.InsertAce($at, $ace)
$new = $raw.GetSddlForm($allSections)
# Kontrol: fjernes den nye regel igen, er resten præcis som før.
$before = (New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl).GetSddlForm($allSections)
if ((Remove-Ace $new $Mask) -ne $before) {
throw 'Intern kontrol fejlede: den nye sikkerhedsbeskrivelse adskiller sig fra den gamle med mere end én regel.'
}
return $new
}
function Read-NamespaceSddl([string] $Namespace) {
$security = Get-WmiObject -Namespace $Namespace -Class __SystemSecurity
$binary = @($null)
$rc = $security.PsBase.InvokeMethod('GetSD', $binary)
if ($rc -ne 0) { throw "Kunne ikke læse sikkerheden på WMI-navnerummet $Namespace ($rc)." }
return (New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList @($binary[0], 0)).GetSddlForm($allSections)
}
function Write-NamespaceSddl([string] $Namespace, [string] $Sddl) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
$bytes = New-Object byte[] $raw.BinaryLength
$raw.GetBinaryForm($bytes, 0)
$security = Get-WmiObject -Namespace $Namespace -Class __SystemSecurity
$rc = $security.PsBase.InvokeMethod('SetSD', @(, $bytes))
if ($rc -ne 0) { throw "Kunne ikke sætte sikkerheden på WMI-navnerummet $Namespace ($rc)." }
}
function Read-ScmSddl {
$sddl = ((& sc.exe sdshow scmanager) | Where-Object { $_ -match '\S' }) -join ''
if ($LASTEXITCODE -ne 0 -or $sddl -notmatch '^D:') { throw "Kunne ikke læse Service Control Managers sikkerhed: $sddl" }
return $sddl
}
function Write-ScmSddl([string] $Sddl) {
$output = & sc.exe sdset scmanager $Sddl
if ($LASTEXITCODE -ne 0) { throw "sc.exe sdset scmanager fejlede ($LASTEXITCODE): $output" }
}
function Get-GroupMembers($Group) {
return @($Group.Invoke('Members') | ForEach-Object {
$bytes = $_.GetType().InvokeMember('objectSid', 'GetProperty', $null, $_, $null)
(New-Object Security.Principal.SecurityIdentifier($bytes, 0)).Value
})
}
# Medlemmer tilføjes og fjernes via deres SID (virker også for NT SERVICE\..., gMSA og computerkonti), ellers via navnet.
function Set-GroupMember($Group, [bool] $Add) {
$paths = @("WinNT://$sid", 'WinNT://' + $Account.Replace('\', '/'))
foreach ($path in $paths) {
try {
if ($Add) { $Group.Add($path) } else { $Group.Remove($path) }
return
} catch {
if ($path -eq $paths[-1]) { throw }
}
}
}
function Save-State {
if ($whatIf) { return }
if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Force -Path $stateDir | Out-Null }
# Kun administratorer og SYSTEM: filen styrer, hvad -Remove fjerner.
$acl = New-Object Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
foreach ($who in 'S-1-5-32-544', 'S-1-5-18') {
$acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule((New-Object Security.Principal.SecurityIdentifier($who)), 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')))
}
Set-Acl -Path $stateDir -AclObject $acl
$state | Export-Clixml -Path $statePath
}
# ---- 2. Læs nuværende tilstand og læg en plan (ændrer intet) ---------------------------------------------------
$wmiMask = 0x21 # Enable Account (0x1) + Remote Enable (0x20)
$scmMask = 0x20015 # CC (0x1) + LC (0x4) + RP (0x10) + RC (READ_CONTROL 0x20000)
$winrmMask = [int] 0x80000000 # GENERIC_READ (GR), ingen Execute (GX)
$plan = New-Object Collections.ArrayList
$original = @{ Time = (Get-Date).ToString('o'); Account = $Account; Sid = $sid; Groups = @{}; Wmi = @{}; Scm = $null; WinRMRoot = $null; Firewall = @{} }
# Lokale grupper, fundet via deres SID, så det virker på dansk, engelsk og andre sprog.
$groups = @(
@('S-1-5-32-558', 'ydelsestællere (CPU, hukommelse, paging)'),
@('S-1-5-32-573', 'hændelseslogge (aldrig Security)'),
@('S-1-5-32-562', 'WMI over DCOM')
)
foreach ($g in $groups) {
try {
$groupName = (New-Object Security.Principal.SecurityIdentifier($g[0])).Translate([Security.Principal.NTAccount]).Value.Split('\')[1]
} catch {
Write-Host " - gruppen $($g[0]) ($($g[1])) findes ikke på denne Windows-version; springes over"
continue
}
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$groupName,group")
$isMember = (Get-GroupMembers $group) -contains $sid
$original.Groups[$g[0]] = $isMember
$added = @($state.Groups) -contains $g[0]
if ($Remove) {
if ($added -and $isMember) { [void] $plan.Add(@{ Kind = 'GroupRemove'; Sid = $g[0]; Name = $groupName; Text = "fjern fra gruppen $groupName" }) }
elseif ($added) { $state.Groups = @($state.Groups | Where-Object { $_ -ne $g[0] }); Write-Host " = ${groupName}: allerede fjernet" }
elseif ($isMember) { Write-Host " = ${groupName}: var medlem før scriptet; bevares" }
} elseif ($isMember) {
Write-Host " = ${groupName}: allerede medlem"
} else {
[void] $plan.Add(@{ Kind = 'GroupAdd'; Sid = $g[0]; Name = $groupName; Text = "medlem af $groupName ($($g[1]))" })
}
}
# WMI-navnerum.
$namespaces = @('root\cimv2')
if (Get-WmiObject -Namespace 'root\Microsoft\Windows' -Class __Namespace -Filter "Name='TaskScheduler'" -ErrorAction SilentlyContinue) {
$namespaces += 'root\Microsoft\Windows\TaskScheduler'
} elseif (-not $Remove) {
Write-Host ' - root\Microsoft\Windows\TaskScheduler findes ikke (før Windows Server 2012): planlagte opgaver kan ikke måles'
}
foreach ($ns in $namespaces) {
$sddl = Read-NamespaceSddl $ns
$original.Wmi[$ns] = $sddl
if ($Remove) {
if (@($state.Wmi) -contains $ns) { [void] $plan.Add(@{ Kind = 'WmiRemove'; Namespace = $ns; Text = "fjern WMI-adgang på $ns" }) }
} elseif (Test-Denied $sddl) {
throw "Kontoen har en Nægt-regel på WMI-navnerummet $ns. Den er sat med vilje; fjern den først, hvis agenten skal læse herfra. Intet er ændret."
} elseif (Test-Allowed $sddl $wmiMask) {
Write-Host " = WMI ${ns}: har allerede adgang"
} else {
[void] (Add-Ace $sddl $wmiMask) # kontrollerer, at ændringen er præcis én regel
[void] $plan.Add(@{ Kind = 'WmiAdd'; Namespace = $ns; Text = "WMI $ns (Enable Account, Remote Enable)" })
}
}
# Service Control Manager: over netværket må ellers kun administratorer liste tjenester.
$scm = Read-ScmSddl
$original.Scm = $scm
if ($Remove) {
if ($state.Scm) { [void] $plan.Add(@{ Kind = 'ScmRemove'; Text = 'fjern læseadgang til Service Control Manager' }) }
} elseif (Test-Denied $scm) {
throw 'Kontoen har en Nægt-regel på Service Control Manager. Den er sat med vilje; fjern den først, hvis agenten skal se tjenester. Intet er ændret.'
} elseif (Test-Allowed $scm $scmMask) {
Write-Host ' = Service Control Manager: har allerede læseadgang'
} else {
[void] (Add-Ace $scm $scmMask)
[void] $plan.Add(@{ Kind = 'ScmAdd'; Text = 'Service Control Manager (CC LC RP RC: forbinde, liste, læse)' })
}
# WinRM (kun med -WinRM, eller når -Remove skal fjerne det, scriptet gav).
if (($WinRM -and -not $Remove) -or ($Remove -and $state.WinRMRoot)) {
$service = Get-Service -Name WinRM -ErrorAction SilentlyContinue
if (-not $service -or $service.Status -ne 'Running') {
throw 'WinRM-tjenesten kører ikke. Start den (winrm quickconfig), eller kør uden -WinRM, så agenten bruger DCOM. Intet er ændret.'
}
$root = (Get-Item WSMan:\localhost\Service\RootSDDL).Value
$original.WinRMRoot = $root
if ($Remove) {
[void] $plan.Add(@{ Kind = 'WinRMRemove'; Text = 'fjern læseadgang i WinRM (RootSDDL)' })
} elseif (Test-Denied $root) {
throw 'Kontoen har en Nægt-regel i WinRM (RootSDDL). Kør uden -WinRM, så agenten bruger DCOM. Intet er ændret.'
} elseif (Test-Allowed $root $winrmMask) {
Write-Host ' = WinRM: har allerede læseadgang'
} else {
[void] (Add-Ace $root $winrmMask)
[void] $plan.Add(@{ Kind = 'WinRMAdd'; Text = 'WinRM (RootSDDL): læseret (GR), ingen Execute' })
}
}
# Firewall: scriptets egne regler, kun fra agentens adresse. Windows' indbyggede regler røres ikke.
$rules = @(
@{ Name = 'SQLInsight-Agent-RPC-EPMAP'; Display = 'SQLInsight Agent: RPC Endpoint Mapper (WMI over DCOM, hændelseslog)'; Port = 'RPCEPMap'; Service = 'rpcss' },
@{ Name = 'SQLInsight-Agent-WMI-DCOM'; Display = 'SQLInsight Agent: WMI over DCOM'; Port = 'RPC'; Service = 'winmgmt' },
@{ Name = 'SQLInsight-Agent-EventLog'; Display = 'SQLInsight Agent: hændelseslog (RPC)'; Port = 'RPC'; Service = 'eventlog' }
)
if ($WinRM) { $rules += @{ Name = 'SQLInsight-Agent-WinRM'; Display = 'SQLInsight Agent: WinRM (HTTP 5985)'; Port = '5985'; Service = $null } }
if ($Remove -and @($state.Firewall).Count -gt 0 -and (Get-Command Get-NetFirewallRule -ErrorAction SilentlyContinue)) {
foreach ($name in @($state.Firewall)) {
$existing = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue
if ($existing) { $original.Firewall[$name] = @(($existing | Get-NetFirewallAddressFilter).RemoteAddress) }
if ($existing) { [void] $plan.Add(@{ Kind = 'FirewallRemove'; Name = $name; Text = "fjern firewallreglen $name" }) }
}
} elseif ($EnableFirewall -and -not $Remove) {
$wanted = @($AgentAddress.Split(',') | ForEach-Object { $_.Trim() })
foreach ($r in $rules) {
$existing = Get-NetFirewallRule -Name $r.Name -ErrorAction SilentlyContinue
if ($existing) {
$current = @(($existing | Get-NetFirewallAddressFilter).RemoteAddress)
$original.Firewall[$r.Name] = $current
if (($current -join ',') -eq ($wanted -join ',')) { Write-Host " = firewall $($r.Name): uændret"; continue }
[void] $plan.Add(@{ Kind = 'FirewallUpdate'; Rule = $r; Address = $wanted; Text = "firewallreglen $($r.Name): kun fra $AgentAddress" })
} else {
[void] $plan.Add(@{ Kind = 'FirewallAdd'; Rule = $r; Address = $wanted; Text = "firewall: $($r.Display) (TCP $($r.Port)), kun fra $AgentAddress" })
}
}
}
if ($plan.Count -eq 0) {
if ($Remove -and -not $whatIf) { Save-State }
Write-Host 'Alt er allerede på plads. Intet er ændret.'
return
}
# ---- 3. Gem det oprindelige, og udfør planen ét skridt ad gangen -----------------------------------------------
$backupPath = Join-Path $stateDir ("$sid-" + (Get-Date).ToString('yyyyMMdd-HHmmss') + '-foer.xml')
if (-not $whatIf) {
Save-State
$original | Export-Clixml -Path $backupPath
Write-Host " Oprindelige indstillinger gemt i $backupPath"
}
foreach ($step in $plan) {
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, $step.Text)) { continue }
try {
switch ($step.Kind) {
'GroupAdd' {
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$($step.Name),group")
Set-GroupMember $group $true
if (-not ((Get-GroupMembers $group) -contains $sid)) { throw "kontoen kom ikke i gruppen $($step.Name)" }
$state.Groups = @($state.Groups) + $step.Sid
}
'GroupRemove' {
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$($step.Name),group")
Set-GroupMember $group $false
$state.Groups = @($state.Groups | Where-Object { $_ -ne $step.Sid })
}
'WmiAdd' {
Write-NamespaceSddl $step.Namespace (Add-Ace (Read-NamespaceSddl $step.Namespace) $wmiMask)
if (-not (Test-Allowed (Read-NamespaceSddl $step.Namespace) $wmiMask)) { throw "adgangen på $($step.Namespace) blev ikke sat" }
$state.Wmi = @($state.Wmi) + $step.Namespace
}
'WmiRemove' {
$sddl = Read-NamespaceSddl $step.Namespace
$new = Remove-Ace $sddl $wmiMask
if ($new -ne $sddl) { Write-NamespaceSddl $step.Namespace $new }
$state.Wmi = @($state.Wmi | Where-Object { $_ -ne $step.Namespace })
}
'ScmAdd' {
Write-ScmSddl (Add-Ace (Read-ScmSddl) $scmMask)
if (-not (Test-Allowed (Read-ScmSddl) $scmMask)) { throw 'læseadgangen til Service Control Manager blev ikke sat' }
$state.Scm = $true
}
'ScmRemove' {
$sddl = Read-ScmSddl
$new = Remove-Ace $sddl $scmMask
if ($new -ne $sddl) { Write-ScmSddl $new }
$state.Scm = $false
}
'WinRMAdd' {
$new = Add-Ace (Get-Item WSMan:\localhost\Service\RootSDDL).Value $winrmMask
Set-Item -Path WSMan:\localhost\Service\RootSDDL -Value $new -Force -WhatIf:$false -Confirm:$false
if (-not (Test-Allowed (Get-Item WSMan:\localhost\Service\RootSDDL).Value $winrmMask)) { throw 'læseretten i WinRM blev ikke sat' }
$state.WinRMRoot = $true
}
'WinRMRemove' {
$sddl = (Get-Item WSMan:\localhost\Service\RootSDDL).Value
$new = Remove-Ace $sddl $winrmMask
if ($new -ne $sddl) { Set-Item -Path WSMan:\localhost\Service\RootSDDL -Value $new -Force -WhatIf:$false -Confirm:$false }
$state.WinRMRoot = $false
}
'FirewallAdd' {
$r = $step.Rule
$arguments = @{ Name = $r.Name; DisplayName = $r.Display; Direction = 'Inbound'; Action = 'Allow'; Protocol = 'TCP'; LocalPort = $r.Port; RemoteAddress = $step.Address; Profile = 'Any' }
if ($r.Service) { $arguments.Program = '%SystemRoot%\system32\svchost.exe'; $arguments.Service = $r.Service }
New-NetFirewallRule @arguments -WhatIf:$false -Confirm:$false | Out-Null
$state.Firewall = @($state.Firewall) + $r.Name
}
'FirewallUpdate' {
Set-NetFirewallRule -Name $step.Rule.Name -RemoteAddress $step.Address -WhatIf:$false -Confirm:$false
if (-not (@($state.Firewall) -contains $step.Rule.Name)) { $state.Firewall = @($state.Firewall) + $step.Rule.Name }
}
'FirewallRemove' {
Remove-NetFirewallRule -Name $step.Name -WhatIf:$false -Confirm:$false
$state.Firewall = @($state.Firewall | Where-Object { $_ -ne $step.Name })
}
}
} catch {
Save-State
throw "Fejlede ved '$($step.Text)': $($_.Exception.Message) Det, der allerede er gjort, står i $statePath; ret fejlen og kør scriptet igen, eller kør det med -Remove. De oprindelige indstillinger står i $backupPath."
}
Save-State
if ($Remove) { Write-Host " - $($step.Text)" } else { Write-Host " + $($step.Text)" }
}
if ($whatIf) { return }
if ($Remove) {
$left = @($state.Groups).Count + @($state.Wmi).Count + @($state.Firewall).Count
if ($state.Scm) { $left++ }
if ($state.WinRMRoot) { $left++ }
if ($left -eq 0) {
Move-Item -Path $statePath -Destination (Join-Path $stateDir ("$sid-" + (Get-Date).ToString('yyyyMMdd-HHmmss') + '-fjernet.xml'))
}
Write-Host 'Færdig. Det, scriptet gav, er fjernet; det, der fandtes før, er bevaret.'
} else {
Write-Host 'Færdig. Test forbindelsen fra SQLInsight (Servere > Windows-servere > serveren > Test forbindelsen).'
}