sots-re/tools/windows-housekeeping/Invoke-Housekeeping.ps1

215 lines
11 KiB
PowerShell

[CmdletBinding()]
param(
[ValidateSet('Inspect', 'Apply', 'EnableExistingAutologon', 'ConfigureAutologon', 'Verify')]
[string]$Mode = 'Inspect',
[string]$OutputPath = "$env:ProgramData\SotsRe\housekeeping-result.json"
)
$ErrorActionPreference = 'Stop'
$autologonUser = 're'
$winlogonPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
$consumerPackages = @('Microsoft.MicrosoftOfficeHub')
$taskPaths = @(
'\Microsoft\OneDrive\OneDrive Per-Machine Standalone Update Task',
'\Microsoft\Windows\Maps\MapsUpdateTask',
'\Microsoft\Windows\Windows Error Reporting\QueueReporting',
'\Microsoft\Windows\Customer Experience Improvement Program\Consolidator',
'\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip',
'\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask',
'\Microsoft\Windows\Windows Defender\Windows Defender Scheduled Scan'
)
$policies = @(
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent'; Name = 'DisableWindowsConsumerFeatures'; Value = 1 },
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent'; Name = 'DisableSoftLanding'; Value = 1 },
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent'; Name = 'DisableWindowsSpotlightFeatures'; Value = 1 },
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent'; Name = 'DisableWindowsCopilot'; Value = 1 },
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Feeds'; Name = 'EnableFeeds'; Value = 0 },
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR'; Name = 'AllowGameDVR'; Value = 0 },
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OneDrive'; Name = 'DisableFileSyncNGSC'; Value = 1 },
@{ Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU'; Name = 'NoAutoRebootWithLoggedOnUsers'; Value = 1 }
)
function Get-TaskByPath([string]$TaskPath) {
$lastSlash = $TaskPath.LastIndexOf('\')
$path = $TaskPath.Substring(0, $lastSlash + 1)
$name = $TaskPath.Substring($lastSlash + 1)
Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue
}
if (-not ('SotsReAutologonNative' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
public static class SotsReAutologonNative {
[StructLayout(LayoutKind.Sequential)]
private struct LSA_OBJECT_ATTRIBUTES {
public int Length;
public IntPtr RootDirectory;
public IntPtr ObjectName;
public int Attributes;
public IntPtr SecurityDescriptor;
public IntPtr SecurityQualityOfService;
}
[StructLayout(LayoutKind.Sequential)]
private struct LSA_UNICODE_STRING {
public ushort Length;
public ushort MaximumLength;
public IntPtr Buffer;
}
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool LogonUser(string user, string domain, string password,
int logonType, int logonProvider, out IntPtr token);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll")]
private static extern uint LsaOpenPolicy(IntPtr systemName, ref LSA_OBJECT_ATTRIBUTES attributes,
uint desiredAccess, out IntPtr policyHandle);
[DllImport("advapi32.dll")]
private static extern uint LsaStorePrivateData(IntPtr policyHandle, ref LSA_UNICODE_STRING key,
ref LSA_UNICODE_STRING value);
[DllImport("advapi32.dll")]
private static extern uint LsaClose(IntPtr handle);
[DllImport("advapi32.dll")]
private static extern uint LsaNtStatusToWinError(uint status);
private static LSA_UNICODE_STRING ToLsaString(string value, out IntPtr buffer) {
byte[] bytes = System.Text.Encoding.Unicode.GetBytes(value);
buffer = Marshal.AllocHGlobal(bytes.Length + 2);
Marshal.Copy(bytes, 0, buffer, bytes.Length);
Marshal.WriteInt16(buffer, bytes.Length, 0);
return new LSA_UNICODE_STRING {
Length = (ushort)bytes.Length,
MaximumLength = (ushort)(bytes.Length + 2),
Buffer = buffer
};
}
private static void ZeroAndFree(IntPtr buffer, int bytes) {
if (buffer == IntPtr.Zero) return;
for (int i = 0; i < bytes; i++) Marshal.WriteByte(buffer, i, 0);
Marshal.FreeHGlobal(buffer);
}
private static void CheckStatus(uint status, string operation) {
if (status != 0) throw new Win32Exception((int)LsaNtStatusToWinError(status), operation);
}
public static void ValidateAndStore(string user, string domain, string password) {
IntPtr token;
if (!LogonUser(user, domain, password, 2, 0, out token))
throw new Win32Exception(Marshal.GetLastWin32Error(), "Credential validation failed");
try { }
finally { CloseHandle(token); }
IntPtr policy = IntPtr.Zero, keyBuffer = IntPtr.Zero, passwordBuffer = IntPtr.Zero;
try {
LSA_OBJECT_ATTRIBUTES attributes = new LSA_OBJECT_ATTRIBUTES();
attributes.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
CheckStatus(LsaOpenPolicy(IntPtr.Zero, ref attributes, 0x00000020, out policy), "LsaOpenPolicy");
LSA_UNICODE_STRING key = ToLsaString("DefaultPassword", out keyBuffer);
LSA_UNICODE_STRING secret = ToLsaString(password, out passwordBuffer);
CheckStatus(LsaStorePrivateData(policy, ref key, ref secret), "LsaStorePrivateData");
}
finally {
if (policy != IntPtr.Zero) LsaClose(policy);
ZeroAndFree(keyBuffer, ("DefaultPassword".Length + 1) * 2);
ZeroAndFree(passwordBuffer, (password.Length + 1) * 2);
}
}
}
'@
}
function Get-Inventory {
$registry = foreach ($policy in $policies) {
$item = Get-ItemProperty -Path $policy.Path -ErrorAction SilentlyContinue
$property = if ($item) { $item.PSObject.Properties[$policy.Name] } else { $null }
$value = if ($property) { $property.Value } else { $null }
[pscustomobject]@{ path = $policy.Path; name = $policy.Name; expected = $policy['Value']; value = $value; compliant = ($value -eq $policy['Value']) }
}
$tasks = foreach ($taskPath in $taskPaths) {
$task = Get-TaskByPath $taskPath
[pscustomobject]@{ path = $taskPath; present = ($null -ne $task); state = if ($task) { $task.State.ToString() } else { $null }; compliant = if ($task) { $task.State -eq 'Disabled' } else { $true } }
}
$processes = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match 'OneDrive|Teams|GameBar|GameBarFTServer|Widgets|M365Copilot' } |
Select-Object ProcessName, Id, SessionId
$webviewProcesses = Get-Process -Name 'msedgewebview2' -ErrorAction SilentlyContinue | Select-Object ProcessName, Id, SessionId
$packages = foreach ($package in $consumerPackages) {
$installed = @(Get-AppxPackage -Name $package -ErrorAction SilentlyContinue)
[pscustomobject]@{ name = $package; present = ($installed.Count -gt 0); compliant = ($installed.Count -eq 0) }
}
$sots = Get-Process -Name 'Sword of the Stars' -ErrorAction SilentlyContinue | Select-Object ProcessName, Id, SessionId
$winlogon = Get-ItemProperty -Path $winlogonPath -ErrorAction SilentlyContinue
$secretPresent = Test-Path 'HKLM:\SECURITY\Policy\Secrets\DefaultPassword'
[pscustomobject]@{
computer = $env:COMPUTERNAME
timestamp_utc = (Get-Date).ToUniversalTime().ToString('o')
mode = $Mode
registry = @($registry)
tasks = @($tasks)
consumer_processes = @($processes)
webview_processes = @($webviewProcesses)
consumer_packages = @($packages)
sots_processes = @($sots)
ssh_service = (Get-Service -Name sshd -ErrorAction SilentlyContinue | Select-Object Name, Status, StartType)
autologon = [pscustomobject]@{
enabled = ($winlogon.AutoAdminLogon -eq '1')
username = $winlogon.DefaultUserName
domain = $winlogon.DefaultDomainName
protected_secret_present = $secretPresent
compliant = (($winlogon.AutoAdminLogon -eq '1') -and ($winlogon.DefaultUserName -eq $autologonUser) -and $secretPresent)
}
}
}
if ($Mode -eq 'Apply') {
foreach ($policy in $policies) {
if (-not (Test-Path -Path $policy.Path)) { New-Item -Path $policy.Path -Force | Out-Null }
New-ItemProperty -Path $policy.Path -Name $policy.Name -PropertyType DWord -Value $policy['Value'] -Force | Out-Null
}
foreach ($taskPath in $taskPaths) {
$task = Get-TaskByPath $taskPath
if ($task -and $task.State -ne 'Disabled') { Disable-ScheduledTask -InputObject $task | Out-Null }
}
Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match 'OneDrive|Teams|GameBar|GameBarFTServer|Widgets|M365Copilot' } |
Stop-Process -Force -ErrorAction SilentlyContinue
foreach ($package in $consumerPackages) {
Get-AppxPackage -Name $package -ErrorAction SilentlyContinue | Remove-AppxPackage -ErrorAction Stop
}
}
if ($Mode -eq 'ConfigureAutologon') {
$password = [Console]::In.ReadToEnd().TrimEnd("`r", "`n")
if ([string]::IsNullOrEmpty($password)) { throw 'No autologon credential was received on standard input.' }
try {
[SotsReAutologonNative]::ValidateAndStore($autologonUser, $env:COMPUTERNAME, $password)
New-ItemProperty -Path $winlogonPath -Name 'AutoAdminLogon' -PropertyType String -Value '1' -Force | Out-Null
New-ItemProperty -Path $winlogonPath -Name 'DefaultUserName' -PropertyType String -Value $autologonUser -Force | Out-Null
New-ItemProperty -Path $winlogonPath -Name 'DefaultDomainName' -PropertyType String -Value $env:COMPUTERNAME -Force | Out-Null
}
finally {
$password = $null
}
}
if ($Mode -eq 'EnableExistingAutologon') {
if (-not (Test-Path 'HKLM:\SECURITY\Policy\Secrets\DefaultPassword')) {
throw 'The protected DefaultPassword secret is absent; refusing to enable autologon.'
}
New-ItemProperty -Path $winlogonPath -Name 'AutoAdminLogon' -PropertyType String -Value '1' -Force | Out-Null
New-ItemProperty -Path $winlogonPath -Name 'DefaultUserName' -PropertyType String -Value $autologonUser -Force | Out-Null
New-ItemProperty -Path $winlogonPath -Name 'DefaultDomainName' -PropertyType String -Value $env:COMPUTERNAME -Force | Out-Null
}
$result = Get-Inventory
$result | Add-Member -NotePropertyName compliant -NotePropertyValue (($result.registry | Where-Object { -not $_.compliant }).Count -eq 0 -and ($result.tasks | Where-Object { -not $_.compliant }).Count -eq 0 -and ($result.consumer_processes.Count -eq 0) -and ($result.consumer_packages | Where-Object { -not $_.compliant }).Count -eq 0 -and (($Mode -ne 'Verify') -or $result.autologon.compliant))
$directory = Split-Path -Parent $OutputPath
New-Item -ItemType Directory -Path $directory -Force | Out-Null
$result | ConvertTo-Json -Depth 6 | Set-Content -Path $OutputPath -Encoding UTF8
$result | ConvertTo-Json -Depth 6
if ($Mode -eq 'Verify' -and -not $result.compliant) { exit 2 }