328 lines
15 KiB
PowerShell
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
|