<# LXS - Harden Windows Description: Apply a baseline security posture (firewall, SMBv1, UAC, Defender, LLMNR/NetBIOS) and audit local accounts. Mirror of linux/tools/harden.sh. Repo: https://git.hyko.cx/hykocx/lxs Usage: harden.ps1 [-Yes] [-NoFirewall] [-NoSmb] [-NoUac] [-NoDefender] [-NoNameResolution] [-NoRestorePoint] [-Help] #> # Load LXS common library (colors, UI helpers, spinner, loggers, guards). # Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and # only hits the network when this script is run standalone. $LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' } if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" } $LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null } if ($LxsLibPath -and (Test-Path $LxsLibPath)) { . $LxsLibPath } else { try { $LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop } catch { Write-Error 'Failed to fetch lib/common.ps1' exit 1 } . ([scriptblock]::Create($LxsLibSource)) } $env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_harden.log' $LxsOpts = Read-LxsFlags -Arguments $args -Known @( '-Yes', '-y', '-NoFirewall', '-NoSmb', '-NoUac', '-NoDefender', '-NoNameResolution', '-NoRestorePoint', '-Help', '-h' ) if (Show-LxsUnknownFlags $LxsOpts) { exit 1 } $AssumeYes = Test-LxsFlag $LxsOpts @('-Yes', '-y') $DoFirewall = -not (Test-LxsFlag $LxsOpts @('-NoFirewall')) $DoSmb = -not (Test-LxsFlag $LxsOpts @('-NoSmb')) $DoUac = -not (Test-LxsFlag $LxsOpts @('-NoUac')) $DoDefender = -not (Test-LxsFlag $LxsOpts @('-NoDefender')) $DoNameResolution = -not (Test-LxsFlag $LxsOpts @('-NoNameResolution')) $DoRestorePoint = -not (Test-LxsFlag $LxsOpts @('-NoRestorePoint')) if (Test-LxsFlag $LxsOpts @('-Help', '-h')) { Write-Host @' Usage: harden.ps1 [options] Options: -Yes, -y Skip the confirmation prompt -NoFirewall Skip the Windows Firewall configuration -NoSmb Skip disabling SMBv1 -NoUac Skip raising the UAC prompt level -NoDefender Skip the Microsoft Defender settings -NoNameResolution Skip disabling LLMNR and NetBIOS-over-TCP/IP -NoRestorePoint Do not create a restore point first -Help, -h Show this help '@ exit 0 } if (-not (Assert-LxsWindows)) { exit 1 } Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args # ═══════════════════════════════════════════════════════════════════════════ # Plan # ═══════════════════════════════════════════════════════════════════════════ Clear-Host Show-LxsBoxTop -Title 'HARDEN WINDOWS' -Right 'ADMIN' Write-Host '' Write-Host 'The following changes will be applied to this machine:' if ($DoFirewall) { Write-Host ' - Firewall: enabled on Domain/Private/Public, inbound blocked by default' } if ($DoSmb) { Write-Host ' - SMBv1: client and server disabled (legacy, exploited by WannaCry/EternalBlue)' } if ($DoUac) { Write-Host ' - UAC: prompt for consent on the secure desktop, never silently elevate' } if ($DoDefender) { Write-Host ' - Defender: real-time protection, PUA blocking, cloud protection, network protection' } if ($DoNameResolution) { Write-Host ' - LLMNR and NetBIOS-over-TCP/IP disabled (blocks classic LAN credential relay)' } Write-Host ' - Audit: local administrators, blank passwords, Guest account (report only)' Write-Host '' if ($DoNameResolution) { Write-Host "$($script:Gray)Note: disabling LLMNR/NetBIOS can break flat-name resolution on small$($script:NC)" Write-Host "$($script:Gray)LANs without a DNS server. Pass -NoNameResolution to keep them.$($script:NC)" Write-Host '' } Show-LxsSeparator Write-Host '' if (-not (Confirm-LxsAction -Question 'Apply this hardening baseline?' -AssumeYes:$AssumeYes)) { Write-LxsInfo 'Cancelled.' exit 0 } if ($DoRestorePoint) { Write-Host '' Write-LxsInfo 'Creating a restore point first...' New-LxsRestorePoint -Description 'LXS before harden' | Out-Null } $changes = 0 $problems = 0 # ═══════════════════════════════════════════════════════════════════════════ # Firewall # ═══════════════════════════════════════════════════════════════════════════ if ($DoFirewall) { Write-Host '' Show-LxsBoxMid 'FIREWALL' Write-Host '' try { Set-NetFirewallProfile -Profile Domain, Private, Public -Enabled True ` -DefaultInboundAction Block -DefaultOutboundAction Allow -ErrorAction Stop Write-LxsOk 'Firewall enabled on all profiles, inbound blocked by default' $changes++ } catch { Write-LxsErr "Firewall configuration failed: $($_.Exception.Message)" $problems++ } try { Get-NetFirewallProfile -ErrorAction Stop | ForEach-Object { Write-Host " $($_.Name.PadRight(8)) enabled=$($_.Enabled) inbound=$($_.DefaultInboundAction) outbound=$($_.DefaultOutboundAction)" } } catch { # Reporting only. } } # ═══════════════════════════════════════════════════════════════════════════ # SMBv1 # ═══════════════════════════════════════════════════════════════════════════ if ($DoSmb) { Write-Host '' Show-LxsBoxMid 'SMBv1' Write-Host '' try { Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force -ErrorAction Stop Write-LxsOk 'SMBv1 server protocol disabled' $changes++ } catch { Write-LxsWarn "Could not disable the SMBv1 server: $($_.Exception.Message)" $problems++ } try { $feature = Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction Stop if ($feature.State -eq 'Enabled') { Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart -ErrorAction Stop | Out-Null Write-LxsOk 'SMBv1 client feature disabled (reboot to complete)' $changes++ } else { Write-LxsOk 'SMBv1 client feature was already disabled' } } catch { Write-LxsWarn "Could not query or disable the SMB1Protocol feature: $($_.Exception.Message)" } } # ═══════════════════════════════════════════════════════════════════════════ # UAC # ═══════════════════════════════════════════════════════════════════════════ if ($DoUac) { Write-Host '' Show-LxsBoxMid 'UAC' Write-Host '' $uacPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' $uacOk = $true # EnableLUA=1 keeps UAC on, ConsentPromptBehaviorAdmin=2 always prompts on # the secure desktop, PromptOnSecureDesktop=1 makes that desktop mandatory. $uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'EnableLUA' -Value 1) -and $uacOk $uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'ConsentPromptBehaviorAdmin' -Value 2) -and $uacOk $uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'PromptOnSecureDesktop' -Value 1) -and $uacOk if ($uacOk) { Write-LxsOk 'UAC set to prompt for consent on the secure desktop' $changes++ } else { $problems++ } } # ═══════════════════════════════════════════════════════════════════════════ # Defender # ═══════════════════════════════════════════════════════════════════════════ if ($DoDefender) { Write-Host '' Show-LxsBoxMid 'DEFENDER' Write-Host '' if (-not (Get-Command Set-MpPreference -ErrorAction SilentlyContinue)) { Write-LxsWarn 'Microsoft Defender cmdlets are not available (third-party AV installed?). Skipped.' } else { $settings = @( @{ Name = 'Real-time protection'; Args = @{ DisableRealtimeMonitoring = $false } }, @{ Name = 'PUA blocking'; Args = @{ PUAProtection = 1 } }, @{ Name = 'Cloud protection'; Args = @{ MAPSReporting = 2 } }, @{ Name = 'Sample submission'; Args = @{ SubmitSamplesConsent = 1 } }, @{ Name = 'Network protection'; Args = @{ EnableNetworkProtection = 1 } }, @{ Name = 'Script scanning'; Args = @{ DisableScriptScanning = $false } }, @{ Name = 'Archive scanning'; Args = @{ DisableArchiveScanning = $false } } ) foreach ($s in $settings) { try { $mpArgs = $s.Args Set-MpPreference @mpArgs -ErrorAction Stop Write-LxsOk "$($s.Name) enabled" $changes++ } catch { # Tamper Protection blocks these writes by design; that is a # stronger guarantee than what we were trying to set. Write-LxsWarn "$($s.Name): $($_.Exception.Message)" } } try { $status = Get-MpComputerStatus -ErrorAction Stop Write-Host '' Write-Host " Antivirus enabled : $($status.AntivirusEnabled)" Write-Host " Real-time protection : $($status.RealTimeProtectionEnabled)" Write-Host " Tamper protection : $($status.IsTamperProtected)" Write-Host " Signature age (days) : $($status.AntivirusSignatureAge)" } catch { # Reporting only. } } } # ═══════════════════════════════════════════════════════════════════════════ # LLMNR + NetBIOS # ═══════════════════════════════════════════════════════════════════════════ if ($DoNameResolution) { Write-Host '' Show-LxsBoxMid 'NAME RESOLUTION' Write-Host '' if (Set-LxsRegistryValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' ` -Name 'EnableMulticast' -Value 0) { Write-LxsOk 'LLMNR disabled' $changes++ } else { $problems++ } # NetbiosOptions: 0 = DHCP default, 1 = enabled, 2 = disabled. $nbPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces' try { $interfaces = @(Get-ChildItem $nbPath -ErrorAction Stop) foreach ($iface in $interfaces) { Set-ItemProperty -Path $iface.PSPath -Name 'NetbiosOptions' -Value 2 -ErrorAction SilentlyContinue } Write-LxsOk "NetBIOS-over-TCP/IP disabled on $($interfaces.Count) interface(s)" $changes++ } catch { Write-LxsWarn "Could not disable NetBIOS-over-TCP/IP: $($_.Exception.Message)" } } # ═══════════════════════════════════════════════════════════════════════════ # Account audit — reports only, changes nothing. # ═══════════════════════════════════════════════════════════════════════════ Write-Host '' Show-LxsBoxMid 'ACCOUNT AUDIT' Write-Host '' try { $admins = @(Get-LocalGroupMember -Group 'Administrators' -ErrorAction Stop) Write-Host "$($script:Cyan)Local administrators:$($script:NC)" foreach ($a in $admins) { Write-Host " - $($a.Name) $($script:Gray)($($a.ObjectClass), $($a.PrincipalSource))$($script:NC)" } if ($admins.Count -gt 2) { Write-Host '' Write-LxsWarn "$($admins.Count) accounts have administrator rights — review whether they all need it." } } catch { Write-LxsWarn "Could not enumerate the Administrators group: $($_.Exception.Message)" } Write-Host '' try { $locals = @(Get-LocalUser -ErrorAction Stop) $enabledNoExpiry = @($locals | Where-Object { $_.Enabled -and $_.PasswordNeverExpires }) $noPassword = @($locals | Where-Object { $_.Enabled -and -not $_.PasswordLastSet -and $_.Name -ne 'DefaultAccount' }) $guest = $locals | Where-Object { $_.Name -eq 'Guest' } if ($noPassword.Count -gt 0) { Write-LxsWarn "Enabled accounts that have never set a password: $($noPassword.Name -join ', ')" } else { Write-LxsOk 'No enabled account is missing a password' } if ($enabledNoExpiry.Count -gt 0) { Write-Host "$($script:Gray) Password never expires: $($enabledNoExpiry.Name -join ', ')$($script:NC)" } if ($guest -and $guest.Enabled) { Write-LxsWarn 'The Guest account is ENABLED — disable it unless you rely on it.' } else { Write-LxsOk 'Guest account is disabled' } } catch { Write-LxsWarn "Could not enumerate local users: $($_.Exception.Message)" } # ═══════════════════════════════════════════════════════════════════════════ # Summary # ═══════════════════════════════════════════════════════════════════════════ Write-Host '' Show-LxsSeparator Write-Host '' Write-LxsOk "$changes hardening change(s) applied" if ($problems -gt 0) { Write-LxsWarn "$problems change(s) failed — see the messages above." } if ($DoSmb) { Write-LxsWarn 'Reboot to finish removing the SMBv1 client feature.' } Write-Host '' exit 0