215 lines
11 KiB
PowerShell
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 }
|