feat(welcome): account + BitLocker + bootstrap services

This commit is contained in:
sysadmin
2026-06-09 02:26:35 +01:00
parent 64b9e3c5f4
commit 62f66490d1
7 changed files with 90 additions and 0 deletions

View File

@@ -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("'", "''");
}

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
namespace SilverOS.Welcome.Core.Apply;
public interface IAccountService { Task CreateAccountsAsync(string user, string password, string adminPassword, CancellationToken ct = default); }

View File

@@ -0,0 +1,2 @@
namespace SilverOS.Welcome.Core.Apply;
public interface IBitLockerService { Task EnableAsync(string pin, CancellationToken ct = default); }

View File

@@ -0,0 +1,2 @@
namespace SilverOS.Welcome.Core.Apply;
public interface IBootstrapService { Task TearDownAsync(string bootstrapUser, CancellationToken ct = default); }

View File

@@ -0,0 +1,45 @@
using Moq;
using SilverOS.Welcome.Core.Apply;
public class ApplyServicesTests
{
private static Mock<IProcessRunner> Ok()
{
var m = new Mock<IProcessRunner>();
m.Setup(r => r.RunAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.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<string>(s =>
s.Contains("New-LocalUser") && s.Contains("alice")), It.IsAny<CancellationToken>()));
run.Verify(r => r.RunAsync("powershell.exe", It.Is<string>(s =>
s.Contains("'SilverOS Admin'") && s.Contains("Administrators")), It.IsAny<CancellationToken>()));
}
[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<string>(s =>
s.Contains("Enable-BitLocker") && s.Contains("TpmAndPinProtector")), It.IsAny<CancellationToken>()));
}
[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<string>(s =>
s.Contains("AutoAdminLogon") && s.Contains("0")), It.IsAny<CancellationToken>()));
run.Verify(r => r.RunAsync("powershell.exe", It.Is<string>(s =>
s.Contains("Remove-LocalUser") && s.Contains("sm-bootstrap")), It.IsAny<CancellationToken>()));
}
}