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>
467 lines
25 KiB
PowerShell
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).'
|
|
}
|