diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/AccountService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/AccountService.cs new file mode 100644 index 0000000..622e30d --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/AccountService.cs @@ -0,0 +1,18 @@ +namespace SilverOS.Welcome.Core.Apply; +public sealed class AccountService(IProcessRunner runner) : IAccountService +{ + public async Task CreateAccountsAsync(string user, string password, string adminPassword, CancellationToken ct = default) + { + // Daily account = Standard User (Users group only — NOT Administrators). + await Ps($"$p=ConvertTo-SecureString '{Esc(password)}' -AsPlainText -Force; " + + $"New-LocalUser -Name '{Esc(user)}' -Password $p -FullName '{Esc(user)}' -AccountNeverExpires; " + + $"Add-LocalGroupMember -Group 'Users' -Member '{Esc(user)}'", ct); + // Separate elevation account. + await Ps($"$a=ConvertTo-SecureString '{Esc(adminPassword)}' -AsPlainText -Force; " + + $"New-LocalUser -Name 'SilverOS Admin' -Password $a -AccountNeverExpires; " + + $"Add-LocalGroupMember -Group 'Administrators' -Member 'SilverOS Admin'", ct); + } + private Task Ps(string script, CancellationToken ct) => + runner.RunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -Command \"{script}\"", ct); + private static string Esc(string s) => s.Replace("'", "''"); +} diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BitLockerService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BitLockerService.cs new file mode 100644 index 0000000..37edef2 --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BitLockerService.cs @@ -0,0 +1,8 @@ +namespace SilverOS.Welcome.Core.Apply; +public sealed class BitLockerService(IProcessRunner runner) : IBitLockerService +{ + public Task EnableAsync(string pin, CancellationToken ct = default) => + runner.RunAsync("powershell.exe", + $"-NoProfile -ExecutionPolicy Bypass -Command \"$p=ConvertTo-SecureString '{pin.Replace("'", "''")}' -AsPlainText -Force; " + + "Enable-BitLocker -MountPoint $env:SystemDrive -EncryptionMethod XtsAes256 -TpmAndPinProtector -Pin $p -SkipHardwareTest\"", ct); +} diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs new file mode 100644 index 0000000..9f059a1 --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs @@ -0,0 +1,13 @@ +namespace SilverOS.Welcome.Core.Apply; +public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService +{ + public async Task TearDownAsync(string bootstrapUser, CancellationToken ct = default) + { + const string key = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'"; + await Ps($"Set-ItemProperty {key} -Name AutoAdminLogon -Value 0; " + + $"Remove-ItemProperty {key} -Name DefaultPassword -EA SilentlyContinue", ct); + await Ps($"Remove-LocalUser -Name '{bootstrapUser}' -EA SilentlyContinue", ct); + } + private Task Ps(string s, CancellationToken ct) => + runner.RunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -Command \"{s}\"", ct); +} diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/IAccountService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/IAccountService.cs new file mode 100644 index 0000000..43715f9 --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/IAccountService.cs @@ -0,0 +1,2 @@ +namespace SilverOS.Welcome.Core.Apply; +public interface IAccountService { Task CreateAccountsAsync(string user, string password, string adminPassword, CancellationToken ct = default); } diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/IBitLockerService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/IBitLockerService.cs new file mode 100644 index 0000000..4a1918f --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/IBitLockerService.cs @@ -0,0 +1,2 @@ +namespace SilverOS.Welcome.Core.Apply; +public interface IBitLockerService { Task EnableAsync(string pin, CancellationToken ct = default); } diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/IBootstrapService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/IBootstrapService.cs new file mode 100644 index 0000000..7fef74f --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/IBootstrapService.cs @@ -0,0 +1,2 @@ +namespace SilverOS.Welcome.Core.Apply; +public interface IBootstrapService { Task TearDownAsync(string bootstrapUser, CancellationToken ct = default); } diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs new file mode 100644 index 0000000..5113487 --- /dev/null +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs @@ -0,0 +1,45 @@ +using Moq; +using SilverOS.Welcome.Core.Apply; + +public class ApplyServicesTests +{ + private static Mock Ok() + { + var m = new Mock(); + m.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new ProcessResult(0, "", "")); + return m; + } + + [Fact] + public async Task AccountService_creates_standard_daily_and_admin() + { + var run = Ok(); + await new AccountService(run.Object).CreateAccountsAsync("alice", "pw1", "adminpw"); + // daily user is a Standard user (added to Users, NOT Administrators) + run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => + s.Contains("New-LocalUser") && s.Contains("alice")), It.IsAny())); + run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => + s.Contains("'SilverOS Admin'") && s.Contains("Administrators")), It.IsAny())); + } + + [Fact] + public async Task BitLockerService_enables_tpm_and_pin() + { + var run = Ok(); + await new BitLockerService(run.Object).EnableAsync("123456"); + run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => + s.Contains("Enable-BitLocker") && s.Contains("TpmAndPinProtector")), It.IsAny())); + } + + [Fact] + public async Task BootstrapService_removes_autologon_and_account() + { + var run = Ok(); + await new BootstrapService(run.Object).TearDownAsync("sm-bootstrap"); + run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => + s.Contains("AutoAdminLogon") && s.Contains("0")), It.IsAny())); + run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => + s.Contains("Remove-LocalUser") && s.Contains("sm-bootstrap")), It.IsAny())); + } +}