Files
lxs/windows/tools/harden.ps1
T

328 lines
15 KiB
PowerShell

<#
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