sqlinsight-pro/tests/SQLInsight.Core.Tests/ServerGrantScriptTests.cs
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

72 lines
3.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}