Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harden Windows Security Module v0.2.7 #161

Merged
merged 82 commits into from
Dec 15, 2023
Merged
Changes from 1 commit
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
562a22b
Added missing position 0 parameter names
HotCakeX Dec 10, 2023
5894f3c
Improved code best practices
HotCakeX Dec 10, 2023
a466e04
Got rid of all the trailing whitespaces
HotCakeX Dec 10, 2023
36cde4c
Version bump
HotCakeX Dec 10, 2023
0875cc3
Adding the same changes from script to module
HotCakeX Dec 10, 2023
7f9565c
More trailing whitespace removal
HotCakeX Dec 10, 2023
c0f2298
Improved position 0 parameters
HotCakeX Dec 10, 2023
b4f2d3c
Parameter position 0 improvements
HotCakeX Dec 10, 2023
ffd1cf0
Parameter improvements
HotCakeX Dec 10, 2023
f6dde74
Update Harden-Windows-Security.ps1
HotCakeX Dec 10, 2023
ebdbf91
More Parameter improvements
HotCakeX Dec 10, 2023
8ea8101
Applied so many best practices in the code
HotCakeX Dec 10, 2023
e88232e
Function best practices improvements
HotCakeX Dec 10, 2023
b51f9e3
Updated function help sections
HotCakeX Dec 10, 2023
15ba37d
Improved error handling and function helps
HotCakeX Dec 11, 2023
7a941e3
General improvements
HotCakeX Dec 11, 2023
eeeb284
Position 0 parameter name definition improvements
HotCakeX Dec 11, 2023
d1cd277
Fixed a conflict of variable names between modules
HotCakeX Dec 11, 2023
01363e7
Improved parameters
HotCakeX Dec 11, 2023
66382d2
Improved comments
HotCakeX Dec 11, 2023
4f8d701
Merge branch 'main' into Harden-Windows-Security-Module-v.0.2.7
HotCakeX Dec 12, 2023
a2ec3c0
Merge branch 'main' into Harden-Windows-Security-Module-v.0.2.7
HotCakeX Dec 13, 2023
38bf594
Removed the Old unused stuff
HotCakeX Dec 13, 2023
4cbea76
typo fixes
HotCakeX Dec 13, 2023
eafe2ca
Fixed typos
HotCakeX Dec 13, 2023
3ef8a7f
Added positional parameter names
HotCakeX Dec 13, 2023
3057b65
Added workspace file to Harden-Windows-Security
HotCakeX Dec 13, 2023
4ef2732
Improved Unprotect-WindowsSecurity cmdlet
HotCakeX Dec 13, 2023
2ee3caf
Added native ConfirmImpact check
HotCakeX Dec 13, 2023
636fc4f
Improved function help and set cmdlet param values
HotCakeX Dec 13, 2023
f2c3de4
Improved function help sections
HotCakeX Dec 13, 2023
d31cf52
improved Secedit path
HotCakeX Dec 13, 2023
d00a495
Update Confirm-SystemCompliance.psm1
HotCakeX Dec 13, 2023
bd88b4f
Improved Confirm-SystemCompliance cmdlet
HotCakeX Dec 14, 2023
4248f81
module restructuring
HotCakeX Dec 14, 2023
33656a3
completed module restructuring
HotCakeX Dec 14, 2023
26e3794
Update Preloader.ps1
HotCakeX Dec 14, 2023
c364ab2
Removed unnecessary lines
HotCakeX Dec 14, 2023
ed17fcd
Improved the hardening script
HotCakeX Dec 14, 2023
dea4816
Reduced some int64s to int16
HotCakeX Dec 14, 2023
2de5e15
Updated both scripts inside and outside of module dir
HotCakeX Dec 14, 2023
2c6c810
Improved Windows feature management section
HotCakeX Dec 14, 2023
561ef3e
Removed trailing whitespace
HotCakeX Dec 14, 2023
cfcc4e0
Improved DISM module usage
HotCakeX Dec 14, 2023
ca1a445
Removed an unnecessary comment
HotCakeX Dec 14, 2023
b9c50bc
Assigned custom colors to optional features part
HotCakeX Dec 14, 2023
1775ad2
function help improvement
HotCakeX Dec 14, 2023
7aa02e8
Improved scheduled task PS code
HotCakeX Dec 14, 2023
793afa7
Changed a policy in Confirm-SystemCompliance
HotCakeX Dec 15, 2023
aef8322
Removed ConsentPromptBehaviorUser policy
HotCakeX Dec 15, 2023
00b199c
removed prompt for ConsentPromptBehaviorUser
HotCakeX Dec 15, 2023
00c0806
Readme update
HotCakeX Dec 15, 2023
c7f1547
Removed untrusted font blocking prompt
HotCakeX Dec 15, 2023
93439de
Removed untrusted font blocking from policies
HotCakeX Dec 15, 2023
1e621cb
Removed compliance check for untrusted font block
HotCakeX Dec 15, 2023
001539d
Update Confirm-SystemCompliance.psm1
HotCakeX Dec 15, 2023
6b6f735
Separated CTRL + ALT + DEL requirement policy
HotCakeX Dec 15, 2023
bde83c7
Fixed progress bars
HotCakeX Dec 15, 2023
dfd7d73
Added support for OS drives with label other than C
HotCakeX Dec 15, 2023
21f82bc
Added changes from module to the main script
HotCakeX Dec 15, 2023
024353f
Removed hardcoded OS label from scheduled task
HotCakeX Dec 15, 2023
ed78fc0
function relocation and version increase
HotCakeX Dec 15, 2023
3475fcd
Removed unnecessary trailing whitespaces
HotCakeX Dec 15, 2023
dec7620
typo fix
HotCakeX Dec 15, 2023
12f4522
Removed untrusted font blocking from the readme
HotCakeX Dec 15, 2023
4a44b8e
Updated readme regarding CTRL + ALT + DEL policy
HotCakeX Dec 15, 2023
e70b56b
Changed SecreString alias to its full type name
HotCakeX Dec 15, 2023
3664a19
Added CSP link to Enhanced mode search policy
HotCakeX Dec 15, 2023
417388d
Added CSP link for Windows time sync interval policy
HotCakeX Dec 15, 2023
0c84084
Added CSP links for the policies in the CSV file
HotCakeX Dec 15, 2023
04a03cd
Added CSP links for how to configure Edge CSPs
HotCakeX Dec 15, 2023
494f024
Updated recommended extension in workspace file
HotCakeX Dec 15, 2023
f36dbad
Hide the output of optional feature removals
HotCakeX Dec 15, 2023
08880cb
Updated a link for consistency
HotCakeX Dec 15, 2023
122f306
Updated workspace file with recommended extension
HotCakeX Dec 15, 2023
90217d5
Updated workspace file with spellchecker
HotCakeX Dec 15, 2023
41a1bd6
Fixed a typo
HotCakeX Dec 15, 2023
6ce6c45
Merge branch 'main' into Harden-Windows-Security-Module-v.0.2.7
HotCakeX Dec 15, 2023
81ac49c
Using stricter type for colors collection
HotCakeX Dec 15, 2023
20986ec
Improved variable casings and types
HotCakeX Dec 15, 2023
c19bfdb
Improved workspace file for spell checking
HotCakeX Dec 15, 2023
9ce985a
Update Harden-Windows-Security Module.code-workspace
HotCakeX Dec 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Applied so many best practices in the code
HotCakeX committed Dec 10, 2023
commit 8ea810158e406c838fd1fc1df25d55b64fe9f02c
223 changes: 147 additions & 76 deletions Harden-Windows-Security Module/Main files/Harden-Windows-Security.ps1
Original file line number Diff line number Diff line change
@@ -77,13 +77,10 @@
🏴 If you have any questions, requests, suggestions etc. about this script, please open a new Discussion or Issue on GitHub
.EXAMPLE
.NOTES
Check out GitHub page for security recommendations: https://github.com/HotCakeX/Harden-Windows-Security
#>

# Get the execution policy for the current process
@@ -112,13 +109,23 @@ $Host.UI.RawUI.WindowTitle = '❤️‍🔥Harden Windows Security❤️‍🔥'
[System.Boolean]$ShouldEnableOptionalDiagnosticData = $false

#region Functions
# Questions function
function Select-Option {
<#
.synopsis
Function to show a prompt to the user to select an option from a list of options
.INPUTS
System.String
System.Management.Automation.SwitchParameter
.PARAMETER Message
Contains the main prompt message
.PARAMETER ExtraMessage
Contains any extra notes for sub-categories
#>
param(
[parameter(Mandatory = $True)][System.String]$Message, # Contains the main prompt message
[parameter(Mandatory = $True)][System.String]$Message,
[parameter(Mandatory = $True)][System.String[]]$Options,
[parameter(Mandatory = $false)][System.Management.Automation.SwitchParameter]$SubCategory,
[parameter(Mandatory = $false)][System.String]$ExtraMessage # Contains any extra notes for sub-categories
[parameter(Mandatory = $false)][System.String]$ExtraMessage
)

$Selected = $null
@@ -160,8 +167,15 @@ function Select-Option {
return $Selected
}

# Function to modify registry
function Edit-Registry {
<#
.SYNOPSIS
Function to modify registry
.INPUTS
System.String
.OUTPUTS
System.Void
#>
param ([System.String]$Path, [System.String]$Key, [System.String]$Value, [System.String]$Type, [System.String]$Action)
If (-NOT (Test-Path -Path $Path)) {
New-Item -Path $Path -Force | Out-Null
@@ -174,25 +188,37 @@ function Edit-Registry {
}
}

# https://devblogs.microsoft.com/scripting/use-function-to-determine-elevation-of-powershell-console/
# Function to test if current session has administrator privileges
Function Test-IsAdmin {
<#
.SYNOPSIS
Function to test if current session has administrator privileges
.LINK
https://devblogs.microsoft.com/scripting/use-function-to-determine-elevation-of-powershell-console/
#>
[System.Security.Principal.WindowsIdentity]$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
[System.Security.Principal.WindowsPrincipal]$Principal = New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList $Identity
$Principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}

# Hiding Invoke-WebRequest progress because it creates lingering visual effect on PowerShell console for some reason
# https://github.com/PowerShell/PowerShell/issues/14348

# https://stackoverflow.com/questions/18770723/hide-progress-of-Invoke-WebRequest
# Create an in-memory module so $ScriptBlock doesn't run in new scope
$null = New-Module {
function Invoke-WithoutProgress {
<#
.SYNOPSIS
Hiding Invoke-WebRequest progress because it creates lingering visual effect on PowerShell console for some reason
.LINK
https://github.com/PowerShell/PowerShell/issues/14348
.LINK
https://stackoverflow.com/questions/18770723/hide-progress-of-Invoke-WebRequest
.INPUTS
System.Management.Automation.ScriptBlock
.OUTPUTS
System.Void
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[scriptblock]$ScriptBlock
[System.Management.Automation.ScriptBlock]$ScriptBlock
)
# Save current progress preference and hide the progress
[System.Management.Automation.ActionPreference]$PrevProgressPreference = $global:ProgressPreference
@@ -208,13 +234,20 @@ $null = New-Module {
}
}

<#
https://stackoverflow.com/questions/48809012/compare-two-credentials-in-powershell
Safely compares two SecureString objects without decrypting them.
Outputs $true if they are equal, or $false otherwise.
#>
function Compare-SecureString {
<#
.SYNOPSIS
Safely compares two SecureString objects without decrypting them.
Outputs $true if they are equal, or $false otherwise.
.LINK
https://stackoverflow.com/questions/48809012/compare-two-credentials-in-powershell
.INPUTS
System.Security.SecureString
.PARAMETER SecureString1
First secure string
.PARAMETER SecureString2
Second secure string to compare with the first secure string
#>
param(
[Security.SecureString]$SecureString1,
[Security.SecureString]$SecureString2
@@ -246,8 +279,24 @@ function Compare-SecureString {
}
}

# Function to write colorful text based on PS edition
Function Write-SmartText {
<#
.SYNOPSIS
Function to write colorful text based on PS edition
.INPUTS
System.String
System.Management.Automation.SwitchParameter
.OUTPUTS
System.String
.PARAMETER CustomColor
The custom color to use to display the text, uses PSStyle
.PARAMETER GenericColor
The generic color to use to display the text, uses Write-Host and legacy colors
.PARAMETER InputText
The text to display in the selected color
.PARAMETER NoNewLineLegacy
Only used with Legacy colors to write them on the same line, used by the function that gets the removable drives for BitLocker Enhanced security level encryption
#>
[CmdletBinding()]
[Alias('WST')]

@@ -268,7 +317,7 @@ Function Write-SmartText {

[parameter(Mandatory = $false)]
[Alias('N')]
[System.Management.Automation.SwitchParameter]$NoNewLineLegacy # Only used with Legacy colors to write them on the same line, used by the function that gets the removable drives for BitLocker Enhanced security level encryption
[System.Management.Automation.SwitchParameter]$NoNewLineLegacy
)

# Determining if PowerShell edition is Core to use modern styling
@@ -322,11 +371,15 @@ Function Write-SmartText {
Write-Host -Object $InputText -ForegroundColor $GenericColor
}
}

}

# Function to get a removable drive to be used by BitLocker category
function Get-AvailableRemovableDrives {
<#
.SYNOPSIS
Function to get a removable drive to be used by BitLocker category
.INPUTS
None. You cannot pipe objects to this function
#>

# An empty array of objects that holds the final removable drives list
[System.Object[]]$AvailableRemovableDrives = @()
@@ -465,9 +518,19 @@ function Get-AvailableRemovableDrives {
Write-Host ('{0,-4}' -f "$ExitCodeRemovableDriveSelection") -NoNewline -ForegroundColor DarkRed
Write-Host -Object '|Skip encryptions altogether' -ForegroundColor DarkRed

# A function to validate the user input
function Confirm-Choice {
param([System.String]$Choice)
<#
.SYNOPSIS
A function to validate the user input
.INPUTS
System.String
.OUTPUTS
System.Boolean
#>
param(
[System.String]$Choice
)

# Initialize a flag to indicate if the input is valid or not
[System.Boolean]$IsValid = $false
# Initialize a variable to store the parsed integer value
@@ -505,6 +568,43 @@ function Get-AvailableRemovableDrives {
return ($($AvailableRemovableDrives[$Choice - 1]).DriveLetter + ':')
}
}

function Block-CountryIP {
<#
.SYNOPSIS
A function that gets a list of IP addresses and a name for them, then adds those IP addresses in the firewall block rules
.NOTES
-RemoteAddress in New-NetFirewallRule accepts array according to Microsoft Docs,
so we use "[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''" to convert the IP lists, which is a single multiline string, into an array
how to query the number of IPs in each rule
(Get-NetFirewallRule -DisplayName "OFAC Sanctioned Countries IP range blocking" -PolicyStore localhost | Get-NetFirewallAddressFilter).RemoteAddress.count
.INPUTS
System.String
System.String[]
.OUTPUTS
System.Void
#>
param (
[System.String[]]$IPList,
[System.String]$ListName
)

# deletes previous rules (if any) to get new up-to-date IP ranges from the sources and set new rules
Remove-NetFirewallRule -DisplayName "$ListName IP range blocking" -PolicyStore localhost -ErrorAction SilentlyContinue

# converts the list which is in string into array
[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''

# makes sure the list isn't empty
if ($IPList.count -eq 0) {
Write-Host -Object "The IP list was empty, skipping $ListName" -ForegroundColor Yellow
break
}

New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Inbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Outbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
}
#endregion functions

if (Test-IsAdmin) {
@@ -527,17 +627,16 @@ if (Test-IsAdmin) {

}

# doing a try-finally block on the entire script so that when CTRL + C is pressed to forcefully exit the script,
# or break is passed, clean up will still happen for secure exit
# doing a try-catch-finally block on the entire script so that when CTRL + C is pressed to forcefully exit the script,
# or break is passed, clean up will still happen for secure exit. Any errors that happens will be thrown
try {
try {
Invoke-WithoutProgress {
[System.DateTime]$global:LatestVersion = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/HotCakeX/Harden-Windows-Security/main/Version.txt'
}
}
catch {
Write-Error "Couldn't verify if the latest version of the script is installed, please check your Internet connection."
break
Throw 'Could not verify if the latest version of the script is installed, please check your Internet connection.'
}
# Check the current hard-coded version against the latest version online
# the messages can technically only be seen if installing the script in standalone mode using old Windows PowerShell
@@ -561,8 +660,7 @@ try {
#region RequirementsCheck
# check if user's OS is Windows Home edition
if ((Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU -eq '101') {
Write-Error -Message 'Windows Home edition detected, exiting...'
break
Throw 'Windows Home edition detected, exiting...'
}

# check if user's OS is the latest build
@@ -577,42 +675,35 @@ try {

# Make sure the current OS build is equal or greater than the required build
if (-NOT ($FullOSBuild -ge $Requiredbuild)) {
Write-Error -Message "You're not using the latest build of the Windows OS. A minimum build of $Requiredbuild is required but your OS build is $FullOSBuild`nPlease go to Windows Update to install the updates and then try again."
break
Throw "You're not using the latest build of the Windows OS. A minimum build of $Requiredbuild is required but your OS build is $FullOSBuild`nPlease go to Windows Update to install the updates and then try again."
}

if (Test-IsAdmin) {
# check to make sure Secure Boot is enabled
if (-NOT (Confirm-SecureBootUEFI)) {
Write-Error -Message 'Secure Boot is not enabled, please go to your UEFI settings to enable it and then try again.'
break
Throw 'Secure Boot is not enabled, please go to your UEFI settings to enable it and then try again.'
}

# check to make sure TPM is available and enabled
[System.Object]$TPM = Get-Tpm
if (-not ($TPM.tpmpresent -and $TPM.tpmenabled)) {
Write-Error -Message 'TPM is not available or enabled, please enable it in UEFI settings and try again.'
break
Throw 'TPM is not available or enabled, please enable it in UEFI settings and try again.'
}

if (-NOT ($MDAVConfigCurrent.AMServiceEnabled -eq $true)) {
Write-Error -Message 'Microsoft Defender Anti Malware service is not enabled, please enable it and then try again.'
break
Throw 'Microsoft Defender Anti Malware service is not enabled, please enable it and then try again.'
}

if (-NOT ($MDAVConfigCurrent.AntispywareEnabled -eq $true)) {
Write-Error -Message 'Microsoft Defender Anti Spyware is not enabled, please enable it and then try again.'
break
Throw 'Microsoft Defender Anti Spyware is not enabled, please enable it and then try again.'
}

if (-NOT ($MDAVConfigCurrent.AntivirusEnabled -eq $true)) {
Write-Error -Message 'Microsoft Defender Anti Virus is not enabled, please enable it and then try again.'
break
Throw 'Microsoft Defender Anti Virus is not enabled, please enable it and then try again.'
}

if ($MDAVConfigCurrent.AMRunningMode -ne 'Normal') {
Write-Error -Message "Microsoft Defender is running in $($MDAVConfigCurrent.AMRunningMode) state, please remove any 3rd party AV and then try again."
break
Throw "Microsoft Defender is running in $($MDAVConfigCurrent.AMRunningMode) state, please remove any 3rd party AV and then try again."
}
}
#endregion RequirementsCheck
@@ -625,7 +716,7 @@ try {
Set-Location -Path $WorkingDir

# Clean up script block
[scriptblock]$CleanUp = {
[System.Management.Automation.ScriptBlock]$CleanUp = {
Set-Location -Path $HOME
Remove-Item -Recurse -Path "$global:UserTempDirectoryPath\HardeningXStuff\" -Force
# Disable progress bars
@@ -729,7 +820,7 @@ try {
Write-Progress -Id 1 -ParentId 0 -Activity 'Downloading files completed.' -Completed
}
catch {
Write-Error "The required files couldn't be downloaded, Make sure you have Internet connection."
Write-Error 'The required files could not be downloaded, Make sure you have Internet connection.' -ErrorAction Continue
foreach ($Job in $Jobs) { Remove-Job -Job $Job -ErrorAction Stop }
&$CleanUp
}
@@ -762,7 +853,7 @@ try {
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Secureboot /v AvailableUpdates /t REG_DWORD /d 0x30 /f

Write-Host -Object 'The required security measures have been applied to the system' -ForegroundColor Green
Write-Warning 'Make sure to restart your device once. After restart, wait for at least 5-10 minutes and perform a 2nd restart to finish applying security measures completely.'
Write-Warning -Message 'Make sure to restart your device once. After restart, wait for at least 5-10 minutes and perform a 2nd restart to finish applying security measures completely.'
} 'No' { break }
'Exit' { &$CleanUp }
}
@@ -1126,7 +1217,7 @@ try {
# check, make sure there is no CD/DVD drives in the system, because Bitlocker throws an error when there is
$CdDvdCheck = (Get-CimInstance -ClassName Win32_CDROMDrive -Property *).MediaLoaded
if ($CdDvdCheck) {
Write-Warning 'Remove any CD/DVD drives or mounted images/ISO from the system and run the Bitlocker category again.'
Write-Warning -Message 'Remove any CD/DVD drives or mounted images/ISO from the system and run the Bitlocker category again.'
# break from the entire BitLocker category and continue to the next category
break BitLockerCategoryLabel
}
@@ -1141,7 +1232,7 @@ try {
}

# A script block that generates recovery code just like the Windows does
[scriptblock]$RecoveryPasswordContentGenerator = {
[System.Management.Automation.ScriptBlock]$RecoveryPasswordContentGenerator = {
param ([System.Object[]]$KeyProtectorsInputFromScriptBlock)

return @"
@@ -2335,26 +2426,6 @@ IMPORTANT: Make sure to keep it in a safe place, e.g., in OneDrive's Personal Va
'Yes' {
Write-Progress -Id 0 -Activity 'Country IP Blocking' -Status "Step $CurrentMainStep/$TotalMainSteps" -PercentComplete ($CurrentMainStep / $TotalMainSteps * 100)

# -RemoteAddress in New-NetFirewallRule accepts array according to Microsoft Docs,
# so we use "[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''" to convert the IP lists, which is a single multiline string, into an array
function Block-CountryIP {
param ([System.String[]]$IPList , [System.String]$ListName)

# deletes previous rules (if any) to get new up-to-date IP ranges from the sources and set new rules
Remove-NetFirewallRule -DisplayName "$ListName IP range blocking" -PolicyStore localhost -ErrorAction SilentlyContinue

# converts the list which is in string into array
[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''

# makes sure the list isn't empty
if ($IPList.count -eq 0) {
Write-Host -Object "The IP list was empty, skipping $ListName" -ForegroundColor Yellow
break
}

New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Inbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Outbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
}
switch (Select-Option -SubCategory -Options 'Yes', 'No' -Message 'Add countries in the State Sponsors of Terrorism list to the Firewall block list?') {
'Yes' {
Invoke-WithoutProgress {
@@ -2370,11 +2441,7 @@ IMPORTANT: Make sure to keep it in a safe place, e.g., in OneDrive's Personal Va
}
Block-CountryIP -IPList $OFACSanctioned -ListName 'OFAC Sanctioned Countries'
} 'No' { break }
}

# how to query the number of IPs in each rule
# (Get-NetFirewallRule -DisplayName "OFAC Sanctioned Countries IP range blocking" -PolicyStore localhost | Get-NetFirewallAddressFilter).RemoteAddress.count

}
} 'No' { break }
'Exit' { &$CleanUp }
}
@@ -2426,6 +2493,10 @@ IMPORTANT: Make sure to keep it in a safe place, e.g., in OneDrive's Personal Va
# ====================================================End of Non-Admin Commands============================================
#endregion Non-Admin-Commands
}
catch {
# Throw whatever error that occured
Throw $_
}
finally {

if (Test-IsAdmin) {
223 changes: 147 additions & 76 deletions Harden-Windows-Security.ps1
Original file line number Diff line number Diff line change
@@ -77,13 +77,10 @@
🏴 If you have any questions, requests, suggestions etc. about this script, please open a new Discussion or Issue on GitHub
.EXAMPLE
.NOTES
Check out GitHub page for security recommendations: https://github.com/HotCakeX/Harden-Windows-Security
#>

# Get the execution policy for the current process
@@ -112,13 +109,23 @@ $Host.UI.RawUI.WindowTitle = '❤️‍🔥Harden Windows Security❤️‍🔥'
[System.Boolean]$ShouldEnableOptionalDiagnosticData = $false

#region Functions
# Questions function
function Select-Option {
<#
.synopsis
Function to show a prompt to the user to select an option from a list of options
.INPUTS
System.String
System.Management.Automation.SwitchParameter
.PARAMETER Message
Contains the main prompt message
.PARAMETER ExtraMessage
Contains any extra notes for sub-categories
#>
param(
[parameter(Mandatory = $True)][System.String]$Message, # Contains the main prompt message
[parameter(Mandatory = $True)][System.String]$Message,
[parameter(Mandatory = $True)][System.String[]]$Options,
[parameter(Mandatory = $false)][System.Management.Automation.SwitchParameter]$SubCategory,
[parameter(Mandatory = $false)][System.String]$ExtraMessage # Contains any extra notes for sub-categories
[parameter(Mandatory = $false)][System.String]$ExtraMessage
)

$Selected = $null
@@ -160,8 +167,15 @@ function Select-Option {
return $Selected
}

# Function to modify registry
function Edit-Registry {
<#
.SYNOPSIS
Function to modify registry
.INPUTS
System.String
.OUTPUTS
System.Void
#>
param ([System.String]$Path, [System.String]$Key, [System.String]$Value, [System.String]$Type, [System.String]$Action)
If (-NOT (Test-Path -Path $Path)) {
New-Item -Path $Path -Force | Out-Null
@@ -174,25 +188,37 @@ function Edit-Registry {
}
}

# https://devblogs.microsoft.com/scripting/use-function-to-determine-elevation-of-powershell-console/
# Function to test if current session has administrator privileges
Function Test-IsAdmin {
<#
.SYNOPSIS
Function to test if current session has administrator privileges
.LINK
https://devblogs.microsoft.com/scripting/use-function-to-determine-elevation-of-powershell-console/
#>
[System.Security.Principal.WindowsIdentity]$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
[System.Security.Principal.WindowsPrincipal]$Principal = New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList $Identity
$Principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}

# Hiding Invoke-WebRequest progress because it creates lingering visual effect on PowerShell console for some reason
# https://github.com/PowerShell/PowerShell/issues/14348

# https://stackoverflow.com/questions/18770723/hide-progress-of-Invoke-WebRequest
# Create an in-memory module so $ScriptBlock doesn't run in new scope
$null = New-Module {
function Invoke-WithoutProgress {
<#
.SYNOPSIS
Hiding Invoke-WebRequest progress because it creates lingering visual effect on PowerShell console for some reason
.LINK
https://github.com/PowerShell/PowerShell/issues/14348
.LINK
https://stackoverflow.com/questions/18770723/hide-progress-of-Invoke-WebRequest
.INPUTS
System.Management.Automation.ScriptBlock
.OUTPUTS
System.Void
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[scriptblock]$ScriptBlock
[System.Management.Automation.ScriptBlock]$ScriptBlock
)
# Save current progress preference and hide the progress
[System.Management.Automation.ActionPreference]$PrevProgressPreference = $global:ProgressPreference
@@ -208,13 +234,20 @@ $null = New-Module {
}
}

<#
https://stackoverflow.com/questions/48809012/compare-two-credentials-in-powershell
Safely compares two SecureString objects without decrypting them.
Outputs $true if they are equal, or $false otherwise.
#>
function Compare-SecureString {
<#
.SYNOPSIS
Safely compares two SecureString objects without decrypting them.
Outputs $true if they are equal, or $false otherwise.
.LINK
https://stackoverflow.com/questions/48809012/compare-two-credentials-in-powershell
.INPUTS
System.Security.SecureString
.PARAMETER SecureString1
First secure string
.PARAMETER SecureString2
Second secure string to compare with the first secure string
#>
param(
[Security.SecureString]$SecureString1,
[Security.SecureString]$SecureString2
@@ -246,8 +279,24 @@ function Compare-SecureString {
}
}

# Function to write colorful text based on PS edition
Function Write-SmartText {
<#
.SYNOPSIS
Function to write colorful text based on PS edition
.INPUTS
System.String
System.Management.Automation.SwitchParameter
.OUTPUTS
System.String
.PARAMETER CustomColor
The custom color to use to display the text, uses PSStyle
.PARAMETER GenericColor
The generic color to use to display the text, uses Write-Host and legacy colors
.PARAMETER InputText
The text to display in the selected color
.PARAMETER NoNewLineLegacy
Only used with Legacy colors to write them on the same line, used by the function that gets the removable drives for BitLocker Enhanced security level encryption
#>
[CmdletBinding()]
[Alias('WST')]

@@ -268,7 +317,7 @@ Function Write-SmartText {

[parameter(Mandatory = $false)]
[Alias('N')]
[System.Management.Automation.SwitchParameter]$NoNewLineLegacy # Only used with Legacy colors to write them on the same line, used by the function that gets the removable drives for BitLocker Enhanced security level encryption
[System.Management.Automation.SwitchParameter]$NoNewLineLegacy
)

# Determining if PowerShell edition is Core to use modern styling
@@ -322,11 +371,15 @@ Function Write-SmartText {
Write-Host -Object $InputText -ForegroundColor $GenericColor
}
}

}

# Function to get a removable drive to be used by BitLocker category
function Get-AvailableRemovableDrives {
<#
.SYNOPSIS
Function to get a removable drive to be used by BitLocker category
.INPUTS
None. You cannot pipe objects to this function
#>

# An empty array of objects that holds the final removable drives list
[System.Object[]]$AvailableRemovableDrives = @()
@@ -465,9 +518,19 @@ function Get-AvailableRemovableDrives {
Write-Host ('{0,-4}' -f "$ExitCodeRemovableDriveSelection") -NoNewline -ForegroundColor DarkRed
Write-Host -Object '|Skip encryptions altogether' -ForegroundColor DarkRed

# A function to validate the user input
function Confirm-Choice {
param([System.String]$Choice)
<#
.SYNOPSIS
A function to validate the user input
.INPUTS
System.String
.OUTPUTS
System.Boolean
#>
param(
[System.String]$Choice
)

# Initialize a flag to indicate if the input is valid or not
[System.Boolean]$IsValid = $false
# Initialize a variable to store the parsed integer value
@@ -505,6 +568,43 @@ function Get-AvailableRemovableDrives {
return ($($AvailableRemovableDrives[$Choice - 1]).DriveLetter + ':')
}
}

function Block-CountryIP {
<#
.SYNOPSIS
A function that gets a list of IP addresses and a name for them, then adds those IP addresses in the firewall block rules
.NOTES
-RemoteAddress in New-NetFirewallRule accepts array according to Microsoft Docs,
so we use "[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''" to convert the IP lists, which is a single multiline string, into an array
how to query the number of IPs in each rule
(Get-NetFirewallRule -DisplayName "OFAC Sanctioned Countries IP range blocking" -PolicyStore localhost | Get-NetFirewallAddressFilter).RemoteAddress.count
.INPUTS
System.String
System.String[]
.OUTPUTS
System.Void
#>
param (
[System.String[]]$IPList,
[System.String]$ListName
)

# deletes previous rules (if any) to get new up-to-date IP ranges from the sources and set new rules
Remove-NetFirewallRule -DisplayName "$ListName IP range blocking" -PolicyStore localhost -ErrorAction SilentlyContinue

# converts the list which is in string into array
[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''

# makes sure the list isn't empty
if ($IPList.count -eq 0) {
Write-Host -Object "The IP list was empty, skipping $ListName" -ForegroundColor Yellow
break
}

New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Inbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Outbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
}
#endregion functions

if (Test-IsAdmin) {
@@ -527,17 +627,16 @@ if (Test-IsAdmin) {

}

# doing a try-finally block on the entire script so that when CTRL + C is pressed to forcefully exit the script,
# or break is passed, clean up will still happen for secure exit
# doing a try-catch-finally block on the entire script so that when CTRL + C is pressed to forcefully exit the script,
# or break is passed, clean up will still happen for secure exit. Any errors that happens will be thrown
try {
try {
Invoke-WithoutProgress {
[System.DateTime]$global:LatestVersion = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/HotCakeX/Harden-Windows-Security/main/Version.txt'
}
}
catch {
Write-Error "Couldn't verify if the latest version of the script is installed, please check your Internet connection."
break
Throw 'Could not verify if the latest version of the script is installed, please check your Internet connection.'
}
# Check the current hard-coded version against the latest version online
# the messages can technically only be seen if installing the script in standalone mode using old Windows PowerShell
@@ -561,8 +660,7 @@ try {
#region RequirementsCheck
# check if user's OS is Windows Home edition
if ((Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU -eq '101') {
Write-Error -Message 'Windows Home edition detected, exiting...'
break
Throw 'Windows Home edition detected, exiting...'
}

# check if user's OS is the latest build
@@ -577,42 +675,35 @@ try {

# Make sure the current OS build is equal or greater than the required build
if (-NOT ($FullOSBuild -ge $Requiredbuild)) {
Write-Error -Message "You're not using the latest build of the Windows OS. A minimum build of $Requiredbuild is required but your OS build is $FullOSBuild`nPlease go to Windows Update to install the updates and then try again."
break
Throw "You're not using the latest build of the Windows OS. A minimum build of $Requiredbuild is required but your OS build is $FullOSBuild`nPlease go to Windows Update to install the updates and then try again."
}

if (Test-IsAdmin) {
# check to make sure Secure Boot is enabled
if (-NOT (Confirm-SecureBootUEFI)) {
Write-Error -Message 'Secure Boot is not enabled, please go to your UEFI settings to enable it and then try again.'
break
Throw 'Secure Boot is not enabled, please go to your UEFI settings to enable it and then try again.'
}

# check to make sure TPM is available and enabled
[System.Object]$TPM = Get-Tpm
if (-not ($TPM.tpmpresent -and $TPM.tpmenabled)) {
Write-Error -Message 'TPM is not available or enabled, please enable it in UEFI settings and try again.'
break
Throw 'TPM is not available or enabled, please enable it in UEFI settings and try again.'
}

if (-NOT ($MDAVConfigCurrent.AMServiceEnabled -eq $true)) {
Write-Error -Message 'Microsoft Defender Anti Malware service is not enabled, please enable it and then try again.'
break
Throw 'Microsoft Defender Anti Malware service is not enabled, please enable it and then try again.'
}

if (-NOT ($MDAVConfigCurrent.AntispywareEnabled -eq $true)) {
Write-Error -Message 'Microsoft Defender Anti Spyware is not enabled, please enable it and then try again.'
break
Throw 'Microsoft Defender Anti Spyware is not enabled, please enable it and then try again.'
}

if (-NOT ($MDAVConfigCurrent.AntivirusEnabled -eq $true)) {
Write-Error -Message 'Microsoft Defender Anti Virus is not enabled, please enable it and then try again.'
break
Throw 'Microsoft Defender Anti Virus is not enabled, please enable it and then try again.'
}

if ($MDAVConfigCurrent.AMRunningMode -ne 'Normal') {
Write-Error -Message "Microsoft Defender is running in $($MDAVConfigCurrent.AMRunningMode) state, please remove any 3rd party AV and then try again."
break
Throw "Microsoft Defender is running in $($MDAVConfigCurrent.AMRunningMode) state, please remove any 3rd party AV and then try again."
}
}
#endregion RequirementsCheck
@@ -625,7 +716,7 @@ try {
Set-Location -Path $WorkingDir

# Clean up script block
[scriptblock]$CleanUp = {
[System.Management.Automation.ScriptBlock]$CleanUp = {
Set-Location -Path $HOME
Remove-Item -Recurse -Path "$global:UserTempDirectoryPath\HardeningXStuff\" -Force
# Disable progress bars
@@ -729,7 +820,7 @@ try {
Write-Progress -Id 1 -ParentId 0 -Activity 'Downloading files completed.' -Completed
}
catch {
Write-Error "The required files couldn't be downloaded, Make sure you have Internet connection."
Write-Error 'The required files could not be downloaded, Make sure you have Internet connection.' -ErrorAction Continue
foreach ($Job in $Jobs) { Remove-Job -Job $Job -ErrorAction Stop }
&$CleanUp
}
@@ -762,7 +853,7 @@ try {
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Secureboot /v AvailableUpdates /t REG_DWORD /d 0x30 /f

Write-Host -Object 'The required security measures have been applied to the system' -ForegroundColor Green
Write-Warning 'Make sure to restart your device once. After restart, wait for at least 5-10 minutes and perform a 2nd restart to finish applying security measures completely.'
Write-Warning -Message 'Make sure to restart your device once. After restart, wait for at least 5-10 minutes and perform a 2nd restart to finish applying security measures completely.'
} 'No' { break }
'Exit' { &$CleanUp }
}
@@ -1126,7 +1217,7 @@ try {
# check, make sure there is no CD/DVD drives in the system, because Bitlocker throws an error when there is
$CdDvdCheck = (Get-CimInstance -ClassName Win32_CDROMDrive -Property *).MediaLoaded
if ($CdDvdCheck) {
Write-Warning 'Remove any CD/DVD drives or mounted images/ISO from the system and run the Bitlocker category again.'
Write-Warning -Message 'Remove any CD/DVD drives or mounted images/ISO from the system and run the Bitlocker category again.'
# break from the entire BitLocker category and continue to the next category
break BitLockerCategoryLabel
}
@@ -1141,7 +1232,7 @@ try {
}

# A script block that generates recovery code just like the Windows does
[scriptblock]$RecoveryPasswordContentGenerator = {
[System.Management.Automation.ScriptBlock]$RecoveryPasswordContentGenerator = {
param ([System.Object[]]$KeyProtectorsInputFromScriptBlock)

return @"
@@ -2335,26 +2426,6 @@ IMPORTANT: Make sure to keep it in a safe place, e.g., in OneDrive's Personal Va
'Yes' {
Write-Progress -Id 0 -Activity 'Country IP Blocking' -Status "Step $CurrentMainStep/$TotalMainSteps" -PercentComplete ($CurrentMainStep / $TotalMainSteps * 100)

# -RemoteAddress in New-NetFirewallRule accepts array according to Microsoft Docs,
# so we use "[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''" to convert the IP lists, which is a single multiline string, into an array
function Block-CountryIP {
param ([System.String[]]$IPList , [System.String]$ListName)

# deletes previous rules (if any) to get new up-to-date IP ranges from the sources and set new rules
Remove-NetFirewallRule -DisplayName "$ListName IP range blocking" -PolicyStore localhost -ErrorAction SilentlyContinue

# converts the list which is in string into array
[System.String[]]$IPList = $IPList -split '\r?\n' -ne ''

# makes sure the list isn't empty
if ($IPList.count -eq 0) {
Write-Host -Object "The IP list was empty, skipping $ListName" -ForegroundColor Yellow
break
}

New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Inbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
New-NetFirewallRule -DisplayName "$ListName IP range blocking" -Direction Outbound -Action Block -LocalAddress Any -RemoteAddress $IPList -Description "$ListName IP range blocking" -EdgeTraversalPolicy Block -PolicyStore localhost
}
switch (Select-Option -SubCategory -Options 'Yes', 'No' -Message 'Add countries in the State Sponsors of Terrorism list to the Firewall block list?') {
'Yes' {
Invoke-WithoutProgress {
@@ -2370,11 +2441,7 @@ IMPORTANT: Make sure to keep it in a safe place, e.g., in OneDrive's Personal Va
}
Block-CountryIP -IPList $OFACSanctioned -ListName 'OFAC Sanctioned Countries'
} 'No' { break }
}

# how to query the number of IPs in each rule
# (Get-NetFirewallRule -DisplayName "OFAC Sanctioned Countries IP range blocking" -PolicyStore localhost | Get-NetFirewallAddressFilter).RemoteAddress.count

}
} 'No' { break }
'Exit' { &$CleanUp }
}
@@ -2426,6 +2493,10 @@ IMPORTANT: Make sure to keep it in a safe place, e.g., in OneDrive's Personal Va
# ====================================================End of Non-Admin Commands============================================
#endregion Non-Admin-Commands
}
catch {
# Throw whatever error that occured
Throw $_
}
finally {

if (Test-IsAdmin) {