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>
72 lines
3.4 KiB
C#
72 lines
3.4 KiB
C#
using System.Text.RegularExpressions;
|
||
using SQLInsight.Core.Servers;
|
||
|
||
namespace SQLInsight.Core.Tests;
|
||
|
||
/// <summary>
|
||
/// db/permissions/grant-windows-server.ps1 as SQLInsight Web shows it: the agent's account filled in safely, and the
|
||
/// script itself only ever gives read rights (the SDDL logic is run in SQLInsight.Servers.Tests/GrantScriptTests).
|
||
/// </summary>
|
||
public sealed partial class ServerGrantScriptTests
|
||
{
|
||
[Fact]
|
||
public void The_account_is_filled_in()
|
||
{
|
||
var script = ServerGrantScript.For(@"FIRMA\AGENT01$");
|
||
|
||
Assert.Contains(@"[string] $Account = 'FIRMA\AGENT01$',", script, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("DOMÆNE\\AGENTSERVER$', #", script, StringComparison.Ordinal);
|
||
Assert.Equal(script, ServerGrantScript.For(@"FIRMA\AGENT01$"));
|
||
Assert.Contains("'O''Brien'", ServerGrantScript.For("O'Brien"), StringComparison.Ordinal);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("X’; Invoke-Evil; ’")] // PowerShell also ends a single-quoted string at ‘ ’ ‚ ‛
|
||
[InlineData("X‘")]
|
||
[InlineData("X‚")]
|
||
[InlineData("X‛")]
|
||
[InlineData("X\r\nInvoke-Evil")]
|
||
public void An_account_that_could_break_out_of_the_string_is_not_filled_in(string account)
|
||
{
|
||
Assert.False(ServerGrantScript.IsSafeAccount(account));
|
||
Assert.Equal(ServerGrantScript.Template(), ServerGrantScript.For(account));
|
||
Assert.DoesNotContain("Invoke-Evil", ServerGrantScript.For(account), StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Only_read_rights_are_given()
|
||
{
|
||
var script = ServerGrantScript.Template();
|
||
|
||
// Groups: Performance Monitor Users, Event Log Readers, Distributed COM Users. Never Administrators or Remote
|
||
// Management Users (which also allows PowerShell Remoting).
|
||
var groups = GroupBlock().Match(script);
|
||
Assert.True(groups.Success);
|
||
Assert.Equal(["S-1-5-32-558", "S-1-5-32-573", "S-1-5-32-562"], Sid().Matches(groups.Value).Select(m => m.Value));
|
||
|
||
// WMI Enable Account + Remote Enable; SCM connect, list, lock status, read control; WinRM read only.
|
||
Assert.Contains("$wmiMask = 0x21 ", script, StringComparison.Ordinal);
|
||
Assert.Contains("$scmMask = 0x20015 ", script, StringComparison.Ordinal);
|
||
Assert.Contains("$winrmMask = [int] 0x80000000 ", script, StringComparison.Ordinal);
|
||
|
||
// Nothing that changes a service, a task, or runs WMI methods; Windows' own firewall rules are not touched.
|
||
foreach (var forbidden in new[]
|
||
{
|
||
"Start-Service", "Stop-Service", "Restart-Service", "Set-Service", "sc.exe config", "sc.exe start", "sc.exe stop",
|
||
"Invoke-WmiMethod", "Invoke-CimMethod", "Enable-NetFirewallRule", "Disable-NetFirewallRule", "Enable-PSRemoting",
|
||
"Set-PSSessionConfiguration", "S-1-5-32-580", "Add-LocalGroupMember",
|
||
})
|
||
{
|
||
Assert.DoesNotContain(forbidden, script, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
Assert.Contains("SupportsShouldProcess", script, StringComparison.Ordinal); // -WhatIf
|
||
Assert.Contains("IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)", script, StringComparison.Ordinal);
|
||
}
|
||
|
||
[GeneratedRegex(@"\$groups = @\((?:.|\n|\r)*?\n\)")]
|
||
private static partial Regex GroupBlock();
|
||
|
||
[GeneratedRegex(@"S-1-5-32-\d+")]
|
||
private static partial Regex Sid();
|
||
}
|