Function Invoke-HardeningKitty {
<#
.SYNOPSIS
Invoke-HardeningKitty - Checks and hardens your Windows configuration
=^._.^=
_( )/ HardeningKitty
Author: Michael Schneider
License: MIT
Required Dependencies: None
Optional Dependencies: None
.DESCRIPTION
HardeningKitty supports hardening of a Windows system. The configuration of the system is
retrieved and assessed using a finding list. In addition, the system can be hardened according
to predefined values. HardeningKitty reads settings from the registry and uses other modules
to read configurations outside the registry.
.PARAMETER FileFindingList
Path to a finding list in CSV format. HardeningKitty has one list each for machine and user settings.
.PARAMETER Mode
The mode Config only retrieves the settings, while the mode Audit performs an assessment of the settings.
The mode HailMary hardens the system according to recommendations of the HardeningKitty list.
.PARAMETER Source
Defines whether the system is configured using Group Policy (GPO) or Microsoft Intune. The information
gathering process differs between these sources.
.PARAMETER EmojiSupport
The use of emoji is activated. The terminal should support this accordingly. Windows Terminal
offers full support.
.PARAMETER Log
The logging function is activated. The script output is additionally logged in a file. The file
name is assigned by HardeningKitty itself and the file is stored in the same directory as the script.
.PARAMETER LogFile
The name and location of the log file can be defined by the user.
.PARAMETER Report
The retrieved settings and their assessment result are stored in CSV format in a machine-readable format.
The file name is assigned by HardeningKitty itself and the file is stored in the same directory as the script.
.PARAMETER ReportFile
The name and location of the report file can be defined by the user.
.PARAMETER Backup
The retrieved settings and their assessment result are stored in CSV format in a machine-readable format with all value to backup your previous config.
.PARAMETER SkipMachineInformation
Information about the system is not queried and displayed. This may be useful while debugging or
using multiple lists on the same system.
.PARAMETER SkipUserInformation
Information about the user is not queried and displayed. This may be useful while debugging or
using multiple lists on the same system.
.PARAMETER SkipLanguageWarning
Do not show the language warning on an no-english Windows system.
.PARAMETER SkipRestorePoint
Do not create a System Restore Point in HailMary mode. HardeningKitty strongly recommends to backup your system before running Hail Mary. However,
creating can be skipped, for example, if HailMary is executed several times in a row. By default, Windows allows a restore point every 24 hours.
Another reason is when HardeningKitty is run as a user and thus lacks privileges.
.PARAMETER Filter
The Filter parameter can be used to filter the hardening list. For this purpose the PowerShell ScriptBlock syntax must be used, for example { $_.ID -eq 4505 }.
The following elements are useful for filtering: ID, Category, Name, Method, and Severity.
.PARAMETER GPOname
The GPOname parameter defines the name of the GPO policy.
.PARAMETER AllowCustomList
Use a finding list that is not verified against the maintainer signature (a custom or modified list) in a write mode (HailMary, GPO).
Running your own list is your own risk.
.EXAMPLE
Invoke-HardeningKitty -Mode Audit -Log -Report
HardeningKitty performs an audit, saves the results and creates a log file
.EXAMPLE
Invoke-HardeningKitty -FileFindingList finding_list_0x6d69636b_user.csv -SkipMachineInformation
HardeningKitty performs an audit with a specific list and does not show machine information
.EXAMPLE
Invoke-HardeningKitty -Mode Config -Report -ReportFile C:\tmp\my_hardeningkitty_report.csv
HardeningKitty uses the default list, and saves the results in a specific file
.EXAMPLE
Invoke-HardeningKitty -Filter { $_.Severity -eq "Medium" }
HardeningKitty uses the default list, and checks only tests with the severity Medium
#>
[CmdletBinding()]
Param (
# Definition of the finding list, default is machine setting list
[String]
$FileFindingList,
# Choose mode, read system config, audit system config, harden system config
[ValidateSet("Audit", "Config", "HailMary", "GPO")]
[String]
$Mode = "Audit",
# Choose source, Group Policy or Microsoft Intune
[ValidateSet("GPO", "Intune")]
[String]
$Source = "GPO",
# Activate emoji support for Windows Terminal
[Switch]
$EmojiSupport,
# Create a log file
[Switch]
$Log,
# Skip machine information, useful when debugging
[Switch]
$SkipMachineInformation,
# Skip user information, useful when debugging
[Switch]
$SkipUserInformation,
# Skip language warning, if you understand the risk
[Switch]
$SkipLanguageWarning,
# Skip creating a System Restore Point during Hail Mary mode
[Switch]
$SkipRestorePoint,
# Define name and path of the log file
[String]
$LogFile,
# Create a report file in CSV format
[Switch]
$Report,
# Define name and path of the report file
[String]
$ReportFile,
# Create a backup config file in CSV format
[Switch]
$Backup,
# Define name and path of the backup file
[String]
$BackupFile,
# Use PowerShell ScriptBlock syntax to filter the finding list
[scriptblock]
$Filter,
# Define name of the GPO name
[String]
$GPOname,
# Use a finding list that is not verified against the maintainer signature
[Switch]
$AllowCustomList
)
Function Write-ProtocolEntry {
<#
.SYNOPSIS
Output of an event with timestamp and different formatting
depending on the level. If the Log parameter is set, the
output is also stored in a file.
#>
[CmdletBinding()]
Param (
[String]
$Text,
[String]
$LogLevel
)
$Time = Get-Date -Format G
Switch ($LogLevel) {
"Info" { $Message = "[*] $Time - $Text"; Write-Host $Message; Break }
"Debug" { $Message = "[-] $Time - $Text"; Write-Host -ForegroundColor Cyan $Message; Break }
"Warning" { $Message = "[?] $Time - $Text"; Write-Host -ForegroundColor Yellow $Message; Break }
"Error" { $Message = "[!] $Time - $Text"; Write-Host -ForegroundColor Red $Message; Break }
"Success" { $Message = "[$] $Time - $Text"; Write-Host -ForegroundColor Green $Message; Break }
"Notime" { $Message = "[*] $Text"; Write-Host -ForegroundColor Gray $Message; Break }
Default { $Message = "[*] $Time - $Text"; Write-Host $Message; }
}
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
}
Function Add-MessageToFile {
<#
.SYNOPSIS
Write message to a file, this function can be used for logs,
reports, backups and more.
#>
[CmdletBinding()]
Param (
[String]
$Text,
[String]
$File
)
try {
Add-Content -Path $File -Value $Text -ErrorAction Stop
} catch {
Write-ProtocolEntry -Text "Error while writing log entries into $File. Aborting..." -LogLevel "Error"
Break
}
}
Function Write-ResultEntry {
<#
.SYNOPSIS
Output of the assessment result with different formatting
depending on the severity level. If emoji support is enabled,
a suitable symbol is used for the severity rating.
#>
[CmdletBinding()]
Param (
[String]
$Text,
[String]
$SeverityLevel
)
If ($EmojiSupport) {
Switch ($SeverityLevel) {
"Passed" { $Emoji = [char]::ConvertFromUtf32(0x1F63A); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Gray $Message; Break }
"Low" { $Emoji = [char]::ConvertFromUtf32(0x1F63C); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Cyan $Message; Break }
"Medium" { $Emoji = [char]::ConvertFromUtf32(0x1F63F); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Yellow $Message; Break }
"High" { $Emoji = [char]::ConvertFromUtf32(0x1F640); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Red $Message; Break }
Default { $Message = "[*] $Text"; Write-Host $Message; }
}
} Else {
Switch ($SeverityLevel) {
"Passed" { $Message = "[+] $Text"; Write-Host -ForegroundColor Gray $Message; Break }
"Low" { $Message = "[-] $Text"; Write-Host -ForegroundColor Cyan $Message; Break }
"Medium" { $Message = "[$] $Text"; Write-Host -ForegroundColor Yellow $Message; Break }
"High" { $Message = "[!] $Text"; Write-Host -ForegroundColor Red $Message; Break }
Default { $Message = "[*] $Text"; Write-Host $Message; }
}
}
}
Function Get-IniContent ($filePath) {
<#
.SYNOPSIS
Read a .ini file into a tree of hashtables
.NOTES
Original source see https://devblogs.microsoft.com/scripting/use-powershell-to-work-with-any-ini-file/
#>
$ini = @{}
switch -regex -file $FilePath {
"^\[(.+)\]" {
# Section
$section = $matches[1]
$ini[$section] = @{}
$CommentCount = 0
}
"^(;.*)$" {
# Comment
$value = $matches[1]
$CommentCount = $CommentCount + 1
$name = "Comment" + $CommentCount
$ini[$section][$name] = $value
}
"(.+?)\s*=(.*)" {
# Key
$name, $value = $matches[1..2]
$ini[$section][$name] = $value
}
}
return $ini
}
Function Out-IniFile($InputObject, $FilePath, $Encoding) {
<#
.SYNOPSIS
Write a hashtable out to a .ini file
.NOTES
Original source see https://devblogs.microsoft.com/scripting/use-powershell-to-work-with-any-ini-file/
#>
$outFile = New-Item -Force -ItemType file -Path $Filepath
foreach ($i in $InputObject.keys) {
if (!($($InputObject[$i].GetType().Name) -eq "Hashtable")) {
#No Sections
Add-Content -Encoding $Encoding -Path $outFile -Value "$i=$($InputObject[$i])"
} else {
#Sections
Add-Content -Encoding $Encoding -Path $outFile -Value "[$i]"
Foreach ($j in ($InputObject[$i].keys | Sort-Object)) {
if ($j -match "^Comment[\d]+") {
Add-Content -Encoding $Encoding -Path $outFile -Value "$($InputObject[$i][$j])"
} else {
Add-Content -Encoding $Encoding -Path $outFile -Value "$j=$($InputObject[$i][$j])"
}
}
Add-Content -Encoding $Encoding -Path $outFile -Value ""
}
}
}
Function Get-HashtableValueDeep {
<#
.SYNOPSIS
Get a value from a tree of hashtables
#>
[CmdletBinding()]
Param (
[Hashtable]
$Table,
[String]
$Path
)
$Key = $Path.Split('\', 2)
$Entry = $Table[$Key[0]]
if ($Entry -is [hashtable] -and $Key.Length -eq 1) {
throw "Path is incomplete (expected a leaf but still on a branch)"
}
if ($Entry -is [hashtable]) {
return Get-HashtableValueDeep $Entry $Key[1];
} else {
if ($Key.Length -eq 1) {
return $Entry
} else {
throw "Path is too long (expected a branch but arrived at a leaf before the end of the path)"
}
}
}
Function Set-HashtableValueDeep {
<#
.SYNOPSIS
Set a value in a tree of hashtables, using recursion.
#>
[CmdletBinding()]
Param (
[Hashtable]
$Table,
[String]
$Path,
[String]
$Value
)
$Key = $Path.Split('\', 2)
$Entry = $Table[$Key[0]]
if ($Key.Length -eq 2) {
if ($null -eq $Entry) {
$Table[$Key[0]] = @{}
} elseif ($Entry -isnot [hashtable]) {
throw "Not hashtable"
}
return Set-HashtableValueDeep -Table $Table[$Key[0]] -Path $Key[1] -Value $Value;
} elseif ($Key.Length -eq 1) {
$Table[$Key[0]] = $Value;
}
}
Function Get-SidFromAccount {
<#
.SYNOPSIS
Translate the account name (user or group) into the Security Identifier (SID)
#>
[CmdletBinding()]
Param (
[String]
$AccountName
)
try {
$AccountObject = New-Object System.Security.Principal.NTAccount($AccountName)
$AccountSid = $AccountObject.Translate([System.Security.Principal.SecurityIdentifier]).Value
} catch {
# If translation fails, return account name
$AccountSid = $AccountName
}
Return $AccountSid
}
Function Get-AccountFromSid {
<#
.SYNOPSIS
Translate the Security Identifier (SID) into the account name (user or group)
#>
[CmdletBinding()]
Param (
[String]
$AccountSid
)
try {
$AccountObject = New-Object System.Security.Principal.SecurityIdentifier ($AccountSid)
$AccountName = $AccountObject.Translate([System.Security.Principal.NTAccount]).Value
} catch {
# If translation fails, return account SID
$AccountName = $AccountSid
}
Return $AccountName
}
Function Translate-SidFromWellkownAccount {
<#
.SYNOPSIS
Translate the well-known account name (user or group) into the Security Identifier (SID)
No attempt is made to get a Domain SID to identify groups such as Domain Admins,
as the possibility for false positives is too great. In this case the account name is returned.
#>
[CmdletBinding()]
Param (
[String]
$AccountName
)
# Get Computer SID and set well-known local user SID
$ComputerSid = ((Get-LocalUser | Select-Object -First 1).SID).AccountDomainSID.ToString()
$LocalAdminSid = $ComputerSid + "-500"
$LocalGuestSid = $ComputerSid + "-501"
Switch ($AccountName) {
"Administrator" { $AccountSid = $LocalAdminSid; Break }
"Guest" { $AccountSid = $LocalGuestSid; Break }
"BUILTIN\Account Operators" { $AccountSid = "S-1-5-32-548"; Break }
"BUILTIN\Administrators" { $AccountSid = "S-1-5-32-544"; Break }
"BUILTIN\Backup Operators" { $AccountSid = "S-1-5-32-551"; Break }
"BUILTIN\Guests" { $AccountSid = "S-1-5-32-546"; Break }
"BUILTIN\Power Users" { $AccountSid = "S-1-5-32-547"; Break }
"BUILTIN\Print Operators" { $AccountSid = "S-1-5-32-550"; Break }
"BUILTIN\Remote Desktop Users" { $AccountSid = "S-1-5-32-555"; Break }
"BUILTIN\Server Operators" { $AccountSid = "S-1-5-32-549"; Break }
"BUILTIN\Users" { $AccountSid = "S-1-5-32-545"; Break }
"Everyone" { $AccountSid = "S-1-1-0"; Break }
"NT AUTHORITY\ANONYMOUS LOGON" { $AccountSid = "S-1-5-7"; Break }
"NT AUTHORITY\Authenticated Users" { $AccountSid = "S-1-5-11"; Break }
"NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS" { $AccountSid = "S-1-5-9"; Break }
"NT AUTHORITY\IUSR" { $AccountSid = "S-1-5-17"; Break }
"NT AUTHORITY\Local account and member of Administrators group" { $AccountSid = "S-1-5-114"; Break }
"NT AUTHORITY\Local account" { $AccountSid = "S-1-5-113"; Break }
"NT AUTHORITY\LOCAL SERVICE" { $AccountSid = "S-1-5-19"; Break }
"NT AUTHORITY\NETWORK SERVICE" { $AccountSid = "S-1-5-20"; Break }
"NT AUTHORITY\SERVICE" { $AccountSid = "S-1-5-6"; Break }
"NT AUTHORITY\SYSTEM" { $AccountSid = "S-1-5-18"; Break }
"NT SERVICE\WdiServiceHost" { $AccountSid = "S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420"; Break }
"NT VIRTUAL MACHINE\Virtual Machines" { $AccountSid = "S-1-5-83-0"; Break }
"RESTRICTED SERVICES\PrintSpoolerService" { $AccountSid = "S-1-5-99-216390572-1995538116-3857911515-2404958512-2623887229"; Break }
"Window Manager\Window Manager Group" { $AccountSid = "S-1-5-90-0"; Break }
Default { $AccountSid = $AccountName }
}
Return $AccountSid
}
Function Write-NotAdminError {
[CmdletBinding()]
param (
[String]
$FindingID,
[String]
$FindingName,
[string]
$FindingMethod
)
$Script:StatsError++
$Message = "ID " + $FindingID + ", " + $FindingName + ", Method " + $FindingMethod + " requires admin privileges. Test skipped."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
}
Function Write-BinaryError {
[CmdletBinding()]
param (
[String]
$Binary,
[String]
$FindingID,
[String]
$FindingName,
[string]
$FindingMethod
)
$Script:StatsError++
$Message = "ID " + $FindingID + ", " + $FindingName + ", Method " + $FindingMethod + " requires $Binary and it was not found. Test skipped."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
}
Function New-HardeningKittyTempDirectory {
<#
.SYNOPSIS
Create a private working directory for temporary files.
.DESCRIPTION
This function creates a dedicated directory with a random name and an ACL restricted to
the current user (inheritance disabled), so temporary files cannot be pre-created,
tampered with or read by other users. This protects the auditpol backup and the secedit exports
which contain security configuration information.
#>
$TempPath = [System.IO.Path]::GetTempPath()
Do {
$DirectoryPath = Join-Path -Path $TempPath -ChildPath ("HardeningKitty_" + [System.IO.Path]::GetRandomFileName())
} Until (-Not (Test-Path -LiteralPath $DirectoryPath))
$Directory = New-Item -Path $DirectoryPath -ItemType Directory -ErrorAction Stop
# Restrict access to the current user and remove inherited permissions
$CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
$Acl = Get-Acl -Path $Directory.FullName
$Acl.SetAccessRuleProtection($true, $false)
$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
$CurrentUser,
[System.Security.AccessControl.FileSystemRights]::FullControl,
([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow
)
$Acl.AddAccessRule($AccessRule)
# secedit.exe /configure opens its database through the Security Configuration Engine,
# which runs as SYSTEM. Without this rule it fails with "Access is denied" (issue #214).
$SystemSid = New-Object System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::LocalSystemSid, $null)
$AccessRuleSystem = New-Object System.Security.AccessControl.FileSystemAccessRule(
$SystemSid,
[System.Security.AccessControl.FileSystemRights]::FullControl,
([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow
)
$Acl.AddAccessRule($AccessRuleSystem)
Set-Acl -Path $Directory.FullName -AclObject $Acl
Return $Directory.FullName
}
Function New-HardeningKittyTempFile {
<#
.SYNOPSIS
Return a path to a uniquely named temporary file inside the private
HardeningKitty working directory.
.DESCRIPTION
The random file name comes from a cryptographically strong source. With
-CreateFile an empty file is created, mirroring the GetTempFileName() behaviour.
Without it only a path is returned, which is required by auditpol.exe
as it refuses to back up into an existing file. -Extension replaces the
random extension, e.g. ".sdb" for a secedit.exe database.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[String]
$Directory,
[Switch]
$CreateFile,
[String]
$Extension
)
Do {
$FileName = [System.IO.Path]::GetRandomFileName()
If ($Extension) {
$FileName = [System.IO.Path]::ChangeExtension($FileName, $Extension)
}
$FilePath = Join-Path -Path $Directory -ChildPath $FileName
} Until (-Not (Test-Path -LiteralPath $FilePath))
If ($CreateFile) {
New-Item -Path $FilePath -ItemType File -ErrorAction Stop | Out-Null
}
Return $FilePath
}
Function Confirm-FindingListIntegrity {
<#
.SYNOPSIS
Verify a finding list against the signed official manifest.
.DESCRIPTION
Official finding lists are attested by lists\hardeningkitty_lists_manifest.psd1, which
maps each official list file name to its SHA-256 hash and is signed with a detached
PKCS#7 signature (hardeningkitty_lists_manifest.psd1.p7s) by the maintainer certificate.
A list is "verified" when the detached signature is intact, the signer thumbprint matches
the pinned thumbprint, and the list SHA-256 matches the manifest entry for its name.
The signature is checked with SignedCms.CheckSignature($true), which validates signature
integrity only and deliberately ignores the certificate chain and validity period. The
trust anchor is the pinned thumbprint, not a public CA, so a self-signed certificate is
sufficient and no timestamp is required.
This function never throws; it always returns a status object with the properties
Verified (bool), Reason (string) and Hash (string, SHA-256 of the list).
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[String]
$ListPath,
[Parameter(Mandatory = $true)]
[String]
$ManifestPath,
[Parameter(Mandatory = $true)]
[String]
$SignaturePath,
[String]
$PinnedThumbprint
)
$Hash = ""
try {
# Hash of the list under test (reported even when verification fails)
If (Test-Path -LiteralPath $ListPath) {
$Hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $ListPath).Hash
}
# Trust anchor must be configured
If ([String]::IsNullOrWhiteSpace($PinnedThumbprint) -or $PinnedThumbprint -eq "REPLACE_WITH_MAINTAINER_CERT_THUMBPRINT") {
Return [pscustomobject] @{ Verified = $false; Reason = "signing thumbprint not configured"; Hash = $Hash }
}
# Manifest and detached signature must be present
If (-Not (Test-Path -LiteralPath $ManifestPath) -or -Not (Test-Path -LiteralPath $SignaturePath)) {
Return [pscustomobject] @{ Verified = $false; Reason = "signed manifest not found"; Hash = $Hash }
}
# The PKCS#7/CMS types (SignedCms, ContentInfo) live in the System.Security assembly,
# which is not loaded by default on Windows PowerShell 5.1 (already present on PS 7).
If (-not ([System.Management.Automation.PSTypeName]'System.Security.Cryptography.Pkcs.SignedCms').Type) {
Add-Type -AssemblyName System.Security
}
# Verify the detached PKCS#7 signature over the manifest bytes (integrity only)
$ManifestBytes = [System.IO.File]::ReadAllBytes($ManifestPath)
$SignatureBytes = [System.IO.File]::ReadAllBytes($SignaturePath)
$ContentInfo = New-Object System.Security.Cryptography.Pkcs.ContentInfo(, $ManifestBytes)
$SignedCms = New-Object System.Security.Cryptography.Pkcs.SignedCms($ContentInfo, $true)
$SignedCms.Decode($SignatureBytes)
$SignedCms.CheckSignature($true)
# Signer must be the pinned maintainer certificate
$SignerThumbprint = $SignedCms.SignerInfos[0].Certificate.Thumbprint
If ($SignerThumbprint -ne $PinnedThumbprint) {
Return [pscustomobject] @{ Verified = $false; Reason = "manifest signed by an untrusted certificate"; Hash = $Hash }
}
# The manifest content is trustworthy only now that its signature is verified
$Manifest = Import-PowerShellDataFile -Path $ManifestPath
$ListName = Split-Path -Path $ListPath -Leaf
$ExpectedHash = $Manifest.Lists[$ListName]
If ([String]::IsNullOrEmpty($ExpectedHash)) {
Return [pscustomobject] @{ Verified = $false; Reason = "not an official list"; Hash = $Hash }
}
If ($ExpectedHash -ne $Hash) {
Return [pscustomobject] @{ Verified = $false; Reason = "list content does not match the signed manifest"; Hash = $Hash }
}
Return [pscustomobject] @{ Verified = $true; Reason = "official"; Hash = $Hash }
} catch {
Return [pscustomobject] @{ Verified = $false; Reason = "verification error: $($_.Exception.Message)"; Hash = $Hash }
}
}
Function ConvertToInt {
[CmdletBinding()]
Param (
[String]
$string
)
$int64 = $null
$int32 = $null
# Attempt to parse the string as an Int32
if ([Int32]::TryParse($string, [ref]$int32)) {
return $int32
}
# Attempt to parse the string as an Int64
if ([Int64]::TryParse($string, [ref]$int64)) {
return $int64
}
# If the string cannot be parsed as either an Int32 or an Int64, throw an error
throw "Cannot convert string '$string' to an integer."
}
Function Get-IntuneResult {
# Check if the finding list has an Intune setting
If ([string]::IsNullOrEmpty($Finding.RegistryPathIntune)) {
$Result = "NotSupported"
} Else {
# Intune user policy settings contain a SID value, we need to replace it
# We get the value from the registry, as the SID must not match the current user's SID
If ([string] $Finding.RegistryPathIntune.contains("{SID}") -or [string] $Finding.RegistryPathDCP.contains("{SID}")) {
$FindingUserSIDPath = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\PolicyManager\current\" | Select-String -Pattern "S-1"
$FindingUserSID = $FindingUserSIDPath.ToString().Trim().Split("\")
# Replace SID in Intune path
If ([string] $Finding.RegistryPathIntune.contains("{SID}")){
$FindingRegistryPathIntuneUser = $($Finding.RegistryPathIntune).Replace("{SID}",$FindingUserSID[5])
$FindingRegistryPathIntune = $FindingRegistryPathIntuneUser
}
# Replace SID in DCP path
If ([string] $Finding.RegistryPathDCP.contains("{SID}")){
$FindingRegistryPathDCPUser = $($Finding.RegistryPathDCP).Replace("{SID}",$FindingUserSID[5])
$FindingRegistryPathDCP = $FindingRegistryPathDCPUser
}
} Else {
$FindingRegistryPathIntune = $Finding.RegistryPathIntune
$FindingRegistryPathDCP = $Finding.RegistryPathDCP
}
# Check if HardeningKitty needs to get the WinningProvider
If ([string]::IsNullOrEmpty($FindingRegistryPathDCP)) {
# Get result directly
If (Test-Path -Path $FindingRegistryPathIntune) {
try {
$Result = Get-ItemPropertyValue -Path $FindingRegistryPathIntune -Name $Finding.RegistryItemIntune
} catch {
$Result = "NotConfigured"
}
} Else {
$Result = "PathNotExists"
}
} Else {
# Check if policy enabled
If (Test-Path -Path $FindingRegistryPathDCP) {
try {
# Get the WinningProvider
$FindingWinningProvider = $Finding.RegistryItemIntune + "_WinningProvider"
$WinningProvider = Get-ItemPropertyValue -Path $FindingRegistryPathDCP -Name $FindingWinningProvider
# Use the WinningProvider and get the result
$FindingRegistryPathIntune = $($FindingRegistryPathIntune).Replace("{GUID}",$WinningProvider)
If (Test-Path -Path $FindingRegistryPathIntune) {
try {
$Result = Get-ItemPropertyValue -Path $FindingRegistryPathIntune -Name $Finding.RegistryItemIntune
} catch {
$Result = "NotConfigured"
}
} Else {
$Result = "WinningProviderNotExists"
}
} catch {
$Result = "NotConfigured"
}
} Else {
$Result = "NotConfigured"
}
}
}
return $Result
}
#
# Binary Locations
#
$BinarySecedit = "C:\Windows\System32\secedit.exe"
$BinaryAuditpol = "C:\Windows\System32\auditpol.exe"
$BinaryNet = "C:\Windows\System32\net.exe"
$BinaryBcdedit = "C:\Windows\System32\bcdedit.exe"
#
# Start Main
#
$HardeningKittyVersion = "0.9.5-1792740671"
#
# Finding list integrity
#
$HardeningKittyListSigningThumbprint = "F47005C8B9BECD8225E13A00A256BEE23DE27673"
$HardeningKittyListManifestName = "hardeningkitty_lists_manifest.psd1"
#
# Log, report and backup file
#
$Hostname = $env:COMPUTERNAME.ToLower()
$FileDate = Get-Date -Format yyyyMMdd-HHmmss
$WinSystemLocale = Get-WinSystemLocale
$PowerShellVersion = "$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)"
If ($FileFindingList.Length -eq 0) {
$ListName = "finding_list_0x6d69636b_machine"
} Else {
$ListName = [System.IO.Path]::GetFileNameWithoutExtension($FileFindingList)
}
If ($Log) {
If ($LogFile.Length -eq 0) {
$LogFile = "hardeningkitty_log_" + $Hostname + "_" + $ListName + "-$FileDate.log"
} ElseIf ($(Split-Path -Path $LogFile).Length -ne 0) {
If ( -Not(Test-Path -Path $(Split-Path $LogFile))) {
$Message = "The path to your log file does not exist."
$Log = $false
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
}
}
If ($Report) {
If ($ReportFile.Length -eq 0) {
$ReportFile = "hardeningkitty_report_" + $Hostname + "_" + $ListName + "-$FileDate.csv"
} ElseIf ($(Split-Path -Path $ReportFile).Length -ne 0) {
If ( -Not(Test-Path -Path $(Split-Path $ReportFile))) {
$Message = "The path to your report file does not exist."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
}
}
If ($Backup) {
If ($BackupFile.Length -eq 0) {
$BackupFile = "hardeningkitty_backup_" + $Hostname + "_" + $ListName + "-$FileDate.csv"
} ElseIf ($(Split-Path -Path $BackupFile).Length -ne 0) {
If ( -Not(Test-Path -Path $(Split-Path $BackupFile))) {
$Message = "The path to your backup file does not exist."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
}
}
$ReportAllResults = @()
$BackupAllResults = @()
#
# Statistics
#
$StatsPassed = 0
$StatsLow = 0
$StatsMedium = 0
$StatsHigh = 0
$StatsTotal = 0
$Script:StatsError = 0
#
# Header
#
Write-Output "`n"
Write-Output " =^._.^="
Write-Output " _( )/ HardeningKitty $HardeningKittyVersion"
Write-Output "`n"
Write-ProtocolEntry -Text "Starting HardeningKitty" -LogLevel "Info"
#
# Machine information
#
If (-not($SkipMachineInformation)) {
Write-Output "`n"
Write-ProtocolEntry -Text "Getting machine information" -LogLevel "Info"
#
# The Get-ComputerInfo cmdlet gets a consolidated object of system
# and operating system properties. This cmdlet was introduced in Windows PowerShell 5.1.
#
If ($PowerShellVersion -le 5.0) {
try {
$OperatingSystem = Get-CimInstance Win32_operatingsystem
$ComputerSystem = Get-CimInstance Win32_ComputerSystem
Switch ($ComputerSystem.domainrole) {
"0" { $Domainrole = "Standalone Workstation"; Break }
"1" { $Domainrole = "Member Workstation"; Break }
"2" { $Domainrole = "Standalone Server"; Break }
"3" { $Domainrole = "Member Server"; Break }
"4" { $Domainrole = "Backup Domain Controller"; Break }
"5" { $Domainrole = "Primary Domain Controller"; Break }
}
$Uptime = (Get-Date) - $OperatingSystem.LastBootUpTime
$Message = "Hostname: " + $OperatingSystem.CSName
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Domain: " + $ComputerSystem.Domain
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Domain role: " + $Domainrole
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Install date: " + $OperatingSystem.InstallDate
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Last Boot Time: " + $OperatingSystem.LastBootUpTime
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Uptime: " + $Uptime
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Windows: " + $OperatingSystem.Caption
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Windows version: " + $OperatingSystem.Version
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Windows build: " + $OperatingSystem.BuildNumber
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "System-locale: " + $WinSystemLocale.Name
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Powershell Version: " + $PowerShellVersion
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
} catch {
Write-ProtocolEntry -Text "Getting machine information failed." -LogLevel "Warning"
}
} Else {
$MachineInformation = Get-ComputerInfo
$Message = "Hostname: " + $MachineInformation.CsDNSHostName
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Domain: " + $MachineInformation.CsDomain
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Domain role: " + $MachineInformation.CsDomainRole
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Install date: " + $MachineInformation.OsInstallDate
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Last Boot Time: " + $MachineInformation.OsLastBootUpTime
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Uptime: " + $MachineInformation.OsUptime
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Windows: " + $MachineInformation.OsName
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Windows edition: " + $MachineInformation.WindowsEditionId
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Windows version: " + $MachineInformation.WindowsVersion
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Windows build: " + $MachineInformation.WindowsBuildLabEx
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "System-locale: " + $WinSystemLocale.Name
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$Message = "Powershell Version: " + $PowerShellVersion
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
}
}
#
# Warning for non-english systems
#
If ($WinSystemLocale.Name -ne "en-US" -and -not($SkipLanguageWarning)) {
Write-Output "`n"
Write-ProtocolEntry -Text "Language warning" -LogLevel "Info"
$Message = "HardeningKitty was developed for the system language 'en-US'. This system uses '" + $WinSystemLocale.Name + "' Language-dependent analyses can sometimes produce false results. Please create an issue if this occurs."
Write-ProtocolEntry -Text $Message -LogLevel "Warning"
}
#
# Intune Warning
#
If ($Source -eq "Intune") {
Write-Output "`n"
Write-ProtocolEntry -Text "Intune warning" -LogLevel "Info"
$Message = "Hello friend, welcome to an adventure. Intune support is still in development and HardeningKitty has a lot of work to do to be able to query and evaluate Intune settings. Since you're already here, please help! Please maintain the finding lists and share information about Intune registry paths and values via discussions or issues. Since Intune support is still under development, do not expect a comprehensive review, the audit mode is not usable until further notice."
Write-ProtocolEntry -Text $Message -LogLevel "Warning"
}
#
# User information
#
If (-not($SkipUserInformation)) {
Write-Output "`n"
Write-ProtocolEntry -Text "Getting user information" -LogLevel "Info"
$Message = "Username: " + [Security.Principal.WindowsIdentity]::GetCurrent().Name
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
$Message = "Is Admin: " + $IsAdmin
Write-ProtocolEntry -Text $Message -LogLevel "Notime"
} Else {
$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
}
#
# Private working directory
# The auditpol backup and the secedit exports are written here.
#
$HardeningKittyTempDir = New-HardeningKittyTempDirectory
#
# Start Config/Audit mode
# The processing is done per category of the finding list.
# The finding list defines which module is used and the arguments and recommended values for the test.
#
If ($Mode -eq "Audit" -or $Mode -eq "Config") {
# A CSV finding list is imported. HardeningKitty has one machine and one user list.
If ($FileFindingList.Length -eq 0) {
$CurrentLocation = $PSScriptRoot
$DefaultList = "$CurrentLocation\lists\finding_list_0x6d69636b_machine.csv"
If (Test-Path -Path $DefaultList) {
$FileFindingList = $DefaultList
} Else {
$Message = "The finding list $DefaultList was not found."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Continue
}
}
$FindingList = Import-Csv -Path $FileFindingList -Delimiter ","
# Verify the finding list against the signed official manifest. In read modes (Audit, Config)
# this is informational only - nothing is applied to the system.
$ListStatus = Confirm-FindingListIntegrity -ListPath $FileFindingList `
-ManifestPath (Join-Path -Path $PSScriptRoot -ChildPath "lists\$HardeningKittyListManifestName") `
-SignaturePath (Join-Path -Path $PSScriptRoot -ChildPath "lists\$HardeningKittyListManifestName.p7s") `
-PinnedThumbprint $HardeningKittyListSigningThumbprint
If ($ListStatus.Verified) {
Write-ProtocolEntry -Text "Finding list verified as official (signature valid). SHA-256: $($ListStatus.Hash)" -LogLevel "Success"
} Else {
Write-ProtocolEntry -Text "Finding list is custom / unverified ($($ListStatus.Reason)). SHA-256: $($ListStatus.Hash)" -LogLevel "Warning"
}
If ($Filter) {
$FindingList = $FindingList | Where-Object -FilterScript $Filter
If ($FindingList.Length -eq 0) {
$Message = "Your filter did not return any results, please adjust the filter so that HardeningKitty has something to work with."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
}
$LastCategory = ""
ForEach ($Finding in $FindingList) {
#
# Reset
#
$Result = ""
$ResultDefaultValue = $false
#
# Category
#
If ($LastCategory -ne $Finding.Category) {
$Message = "Starting Category " + $Finding.Category
Write-Output "`n"
Write-ProtocolEntry -Text $Message -LogLevel "Info"
$LastCategory = $Finding.Category
}
#
# Get Registry Item
# Registry entries can be read with a native PowerShell function. The retrieved value is evaluated later.
# If the registry entry is not available, a default value is used. This must be specified in the finding list.
#
If ($Finding.Method -eq 'Registry') {
If ($Source -eq 'GPO') {
If (Test-Path -Path $Finding.RegistryPath) {
try {
$Result = Get-ItemPropertyValue -Path $Finding.RegistryPath -Name $Finding.RegistryItem
# Join the result with ";" character if result is an array
if ($Result -is [system.array] -and ($Finding.RegistryItem -eq "Machine" -Or $Finding.RegistryItem -eq "EccCurves" -Or $Finding.RegistryItem -eq "NullSessionPipes" -Or $Finding.RegistryItem -eq "NullSessionShares")){
$Result = $Result -join ";"
}
} catch {
If ($Backup) {
# If an error occurs and the backup mode is enabled, we consider that this policy does not exist
# and put "-NODATA-" as result to identify it as non-existing policy
$Result = "-NODATA-"
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
} Else {
If ($Backup) {
# If this policy does not exist and the backup mode is enabled, we
# put "-NODATA-" as result to identify it as non-existing policy
$Result = "-NODATA-"
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
# Multiline Registry Keys need a semicolon instead of a space
If ($Finding.RegistryItem -eq "Machine") {
$Result = $Result.Replace(";", " ")
}
}
}
} ElseIf ($Source -eq 'Intune') {
$Result = Get-IntuneResult
}
}
#
# Get secedit policy
# Secedit configures and analyzes system security, results are written
# to a file, which means HardeningKitty must create a temporary file
# and afterwards delete it. HardeningKitty is very orderly.
#
ElseIf ($Finding.Method -eq 'secedit') {
# Check if Secedit binary is available, skip test if not
If (-Not (Test-Path $BinarySecedit)) {
Write-BinaryError -Binary $BinarySecedit -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$TempFileName = New-HardeningKittyTempFile -Directory $HardeningKittyTempDir -CreateFile
$Area = "";
Switch ($Finding.Category) {
"Account Policies" { $Area = "SECURITYPOLICY"; Break }
"Security Options" { $Area = "SECURITYPOLICY"; Break }
}
&$BinarySecedit /export /cfg $TempFileName /areas $Area | Out-Null
$Data = Get-IniContent $TempFileName
$Value = Get-HashtableValueDeep $Data $Finding.MethodArgument
if ($null -eq $Value) {
$Result = $null
} else {
$Result = $Value -as [int]
}
Remove-Item $TempFileName
}
#
# Get Registry List and search for item
# Depending on the registry structure, the value cannot be accessed directly, but must be found within a data structure
# If the registry entry is not available, a default value is used. This must be specified in the finding list.
#
ElseIf ($Finding.Method -eq 'RegistryList') {
If ($Source -eq 'GPO') {
If (Test-Path -Path $Finding.RegistryPath) {
try {
$ResultList = Get-ItemProperty -Path $Finding.RegistryPath
If ($ResultList | Where-Object { $_ -like "*" + $Finding.RegistryItem + "*" }) {
$Result = $Finding.RegistryItem
} Else {
$Result = "-NODATA-"
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
} Else {
If ($Backup) {
# If this policy does not exist and the backup mode is enabled, we
# put "-NODATA-" as result to identify it as non-existing policy
$Result = "-NODATA-"
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
} ElseIf ($Source -eq 'Intune') {
$Result = Get-IntuneResult
}
}
#
# Get Audit Policy
# The output of auditpol.exe is parsed and will be evaluated later.
# The desired value is not output directly, some output lines can be ignored
# and are therefore skipped. If the output changes, the parsing must be adjusted :(
#
ElseIf ($Finding.Method -eq 'auditpol') {
# Check if Auditpol binary is available, skip test if not
If (-Not (Test-Path $BinaryAuditpol)) {
Write-BinaryError -Binary $BinaryAuditpol -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
try {
$SubCategory = $Finding.MethodArgument
# auditpol.exe does not write a backup into an existing file, so a path is
# generated without creating the file
$TempFileName = New-HardeningKittyTempFile -Directory $HardeningKittyTempDir
&$BinaryAuditpol /backup /file:$TempFileName > $null
$ResultOutputLoad = Get-Content $TempFileName
foreach ($line in $ResultOutputLoad) {
$table = $line.Split(",")
if ($table[3] -eq $SubCategory) {
# Translate setting value (works only for English list, so this is workaround)
Switch ($table[6]) {
"0" { $Result = "No Auditing"; Break }
"1" { $Result = "Success"; Break }
"2" { $Result = "Failure"; Break }
"3" { $Result = "Success and Failure"; Break }
}
}
}
# House cleaning
Remove-Item $TempFileName
Clear-Variable -Name ("ResultOutputLoad", "table")
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Get Account Policy
# The output of net.exe is parsed and will be evaluated later.
# It may be necessary to use the /domain parameter when calling net.exe.
# The values of the user executing the script are read out. These may not match the password policy.
#
ElseIf ($Finding.Method -eq 'accountpolicy') {
If ($Source -eq 'Intune') {
$Result = Get-IntuneResult
}
If ($Source -eq 'GPO' -or $Result -eq 'NotSupported') {
# Check if net binary is available, skip test if not
If (-Not (Test-Path $BinaryNet)) {
Write-BinaryError -Binary $BinaryNet -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
try {
$ResultOutput = &$BinaryNet accounts
# "Parse" account policy
Switch ($Finding.Name) {
"Force user logoff how long after time expires" { $ResultOutput[0] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Network security: Force logoff when logon hours expires" { $ResultOutput[0] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Minimum password age" { $ResultOutput[1] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Maximum password age" { $ResultOutput[2] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Minimum password length" { $ResultOutput[3] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Length of password history maintained" { $ResultOutput[4] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Account lockout threshold" { $ResultOutput[5] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Account lockout duration" { $ResultOutput[6] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
"Reset account lockout counter" { $ResultOutput[7] -match '([a-zA-Z:, /-]+) ([a-z0-9, ]+)' | Out-Null; $Result = $Matches[2]; Break }
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
}
#
# Get Local Account Information
# The PowerShell function Get-LocalUser is used for this.
# In order to get the correct user, the query is made via the SID,
# the base value of the computer must first be retrieved.
#
ElseIf ($Finding.Method -eq 'localaccount') {
try {
# Get Computer SID
$ComputerSid = ((Get-LocalUser | Select-Object -First 1).SID).AccountDomainSID.ToString()
# Get User Status
$Sid = $ComputerSid + "-" + $Finding.MethodArgument
$ResultOutput = Get-LocalUser -SID $Sid
If ($Finding.Name.Contains("account status")) {
$Result = $ResultOutput.Enabled
} ElseIf ($Finding.Name.Contains("Rename")) {
$Result = $ResultOutput.Name
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# User Rights Assignment
# This method was first developed with the tool accessck.exe, hence the name.
# Due to compatibility problems in languages other than English, secedit.exe is
# now used to read the User Rights Assignments.
#
# Secedit configures and analyzes system security, results are written
# to a file, which means HardeningKitty must create a temporary file
# and afterwards delete it. HardeningKitty is very orderly.
#
ElseIf ($Finding.Method -eq 'accesschk') {
# Check if Secedit binary is available, skip test if not
If (-Not (Test-Path $BinarySecedit)) {
Write-BinaryError -Binary $BinarySecedit -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$Area = "USER_RIGHTS"
$TempFileName = New-HardeningKittyTempFile -Directory $HardeningKittyTempDir -CreateFile
try {
&$BinarySecedit /export /cfg $TempFileName /areas $Area | Out-Null
# -SimpleMatch: MethodArgument comes from the finding list and must be treated
# as literal text, not a regular expression
$ResultOutputRaw = Get-Content -Encoding unicode $TempFileName | Select-String -SimpleMatch $Finding.MethodArgument
If ($null -eq $ResultOutputRaw) {
$Result = ""
} Else {
$ResultOutputList = $ResultOutputRaw.ToString().split("=").Trim()
$Result = $ResultOutputList[1] -Replace "\*", ""
$Result = $Result -Replace ",", ";"
}
} catch {
# If secedit did not work, throw an error instead of using the DefaultValue
$Script:StatsError++
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", secedit.exe could not read the configuration. Test skipped."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Continue
}
Remove-Item $TempFileName
}
#
# Windows Optional Feature
# Yay, a native PowerShell function! The status of the feature can easily be read out directly.
#
ElseIf ($Finding.Method -eq 'WindowsOptionalFeature') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
try {
$ResultOutput = Get-WindowsOptionalFeature -Online -FeatureName $Finding.MethodArgument
$Result = $ResultOutput.State
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Get CimInstance and search for item
# Via a CIM instance classes can be read from the CIM server.
# Afterwards, you have to search for the correct property within the class.
#
ElseIf ($Finding.Method -eq 'CimInstance') {
try {
$ResultList = Get-CimInstance -ClassName $Finding.ClassName -Namespace $Finding.Namespace
$Property = $Finding.Property
If ($ResultList.$Property | Where-Object { $_ -like "*" + $Finding.RecommendedValue + "*" }) {
$Result = $Finding.RecommendedValue
} Else {
$Result = "Not available"
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# BitLocker Drive Encryption
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
#
ElseIf ($Finding.Method -eq 'BitLockerVolume') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
try {
$ResultOutput = Get-BitLockerVolume -MountPoint $Env:SystemDrive
If ($ResultOutput.VolumeType -eq 'OperatingSystem') {
$ResultArgument = $Finding.MethodArgument
$Result = $ResultOutput.$ResultArgument
} Else {
$Result = "Manual check required"
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# PowerShell Language Mode
# This is a single purpose function, the desired configuration is output directly.
#
ElseIf ($Finding.Method -eq 'LanguageMode') {
try {
$ResultOutput = $ExecutionContext.SessionState.LanguageMode
$Result = $ResultOutput
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Microsoft Defender Status
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
#
ElseIf ($Finding.Method -eq 'MpComputerStatus') {
try {
$ResultOutput = Get-MpComputerStatus
$ResultArgument = $Finding.MethodArgument
$Result = $ResultOutput.$ResultArgument
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Microsoft Defender Preferences
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
#
ElseIf ($Finding.Method -eq 'MpPreference') {
try {
$ResultOutput = Get-MpPreference
$ResultArgument = $Finding.MethodArgument
$Result = $ResultOutput.$ResultArgument
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Microsoft Defender Preferences - Attack surface reduction rules (ASR rules)
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
#
ElseIf ($Finding.Method -eq 'MpPreferenceAsr') {
try {
$ResultOutput = Get-MpPreference
$ResultAsrIds = $ResultOutput.AttackSurfaceReductionRules_Ids
$ResultAsrActions = $ResultOutput.AttackSurfaceReductionRules_Actions
$Result = $Finding.DefaultValue
$Counter = 0
ForEach ($AsrRule in $ResultAsrIds) {
If ($AsrRule -eq $Finding.MethodArgument) {
$Result = $ResultAsrActions[$Counter]
Continue
}
$Counter++
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Microsoft Defender Preferences - Exclusion lists
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
#
ElseIf ($Finding.Method -eq 'MpPreferenceExclusion') {
# Check if the user has admin rights, skip test if not
# Normal users are not allowed to get exclusions
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
try {
$ResultOutput = Get-MpPreference
$ExclusionType = $Finding.MethodArgument
$ResultExclusions = $ResultOutput.$ExclusionType
ForEach ($Exclusion in $ResultExclusions) {
$Result += $Exclusion + ";"
}
# Remove last character
$Result = $Result -replace ".$"
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Exploit protection (System)
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
# Since the object has several dimensions and there is only one dimension
# in the finding list (lazy) a workaround with split must be done...
#
ElseIf ($Finding.Method -eq 'Processmitigation') {
try {
$ResultOutput = Get-ProcessMitigation -System
$ResultArgumentArray = $Finding.MethodArgument.Split(".")
$ResultArgument0 = $ResultArgumentArray[0]
$ResultArgument1 = $ResultArgumentArray[1]
$Result = $ResultOutput.$ResultArgument0.$ResultArgument1
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Exploit protection (Application)
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
# Since the object has several dimensions and there is only one dimension
# in the finding list (lazy) a workaround with split must be done...
#
ElseIf ($Finding.Method -eq 'ProcessmitigationApplication') {
try {
$ResultArgumentArray = $Finding.MethodArgument.Split("/")
$ResultOutput = Get-ProcessMitigation -Name $ResultArgumentArray[0]
$ResultArgument0 = $ResultArgumentArray[1]
$ResultArgument1 = $ResultArgumentArray[2]
$Result = $ResultOutput.$ResultArgument0.$ResultArgument1
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# bcdedit
# Again, the output of a tool must be searched and parsed. Ugly...
#
ElseIf ($Finding.Method -eq 'bcdedit') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if Bcdedit binary is available, skip test if not
If (-Not (Test-Path $BinaryBcdedit)) {
Write-BinaryError -Binary $BinaryBcdedit -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
try {
$ResultOutput = &$BinaryBcdedit
$ResultOutput = $ResultOutput | Where-Object { $_ -like "*" + $Finding.RecommendedValue + "*" }
If ($ResultOutput -match ' ([a-z,A-Z]+)') {
$Result = $Matches[1]
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# FirewallRule
# Search for a specific firewall rule with a given name
#
ElseIf ($Finding.Method -eq 'FirewallRule') {
try {
$ResultOutput = Get-NetFirewallRule -PolicyStore ActiveStore -DisplayName $Finding.Name 2> $null
$Result = $ResultOutput.Enabled
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Service
# Check the status of a service
#
ElseIf ($Finding.Method -eq 'service') {
try {
$ResultOutput = Get-Service -Name $Finding.MethodArgument 2> $null
# Use only processes, not drivers
# https://learn.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicetype
If ($ResultOutput.ServiceType.value__ -ge 16) {
$Result = $ResultOutput.StartType
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Scheduled Task
# Check the status of a scheduled task
#
ElseIf ($Finding.Method -eq 'ScheduledTask') {
try {
$ResultOutput = Get-ScheduledTask -TaskName $Finding.MethodArgument 2> $null
$Result = $ResultOutput.State
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
#
# Compare result value and recommendation
# The finding list specifies the test, as well as the recommended values.
# There are two output formats, one for command line output and one for the CSV file.
#
If ($Mode -eq "Audit") {
#
# User Right Assignment
# For multilingual support, a SID translation takes place and then the known SID values are compared with each other.
# The results are already available as SID (from secedit) and therefore the specifications are now also translated and still sorted.
#
If ($Finding.Method -eq 'accesschk') {
$SaveRecommendedValue = $Finding.RecommendedValue
If ($Result -ne '') {
$ListRecommended = $Finding.RecommendedValue.Split(";")
$ListRecommendedSid = @()
# SID Translation
ForEach ($AccountName in $ListRecommended) {
$AccountSid = Translate-SidFromWellkownAccount -AccountName $AccountName
$ListRecommendedSid += $AccountSid
}
# Sort SID List
$ListRecommendedSid = $ListRecommendedSid | Sort-Object
# Build String
ForEach ($AccountName in $ListRecommendedSid) {
[String] $RecommendedValueSid += $AccountName + ";"
}
$RecommendedValueSid = $RecommendedValueSid -replace ".$"
$Finding.RecommendedValue = $RecommendedValueSid
Clear-Variable -Name ("RecommendedValueSid")
}
}
#
# Exception handling for special registry keys
# Machine => Network access: Remotely accessible registry paths
# Hardened UNC Paths => Remove spaces in result and recommendation only if result is not null or empty
#
If ($Source -eq "GPO") {
If ($Finding.Method -eq 'Registry' -and $Finding.RegistryItem -eq "Machine") {
# $Finding.RecommendedValue = $Finding.RecommendedValue.Replace(";", " ")
} ElseIf ($Finding.Method -eq 'Registry' -and $Finding.RegistryPath -eq "HKLM:\Software\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths") {
If (![string]::IsNullOrEmpty($Result)) {
$Result = $Result.Replace(" ", "")
}
$Finding.RecommendedValue = $Finding.RecommendedValue.Replace(" ", "")
}
}
#
# Handling for registry keys with an "advanced" format
#
# Define list of registry items
$RegistryItemCollection = @("ConfigureRpcConnectionPolicy", "ConfigureRpcListenerPolicy", "CSE_Registry", "CSE_Security", "PointAndPrintRestrictions",
"SpecifyMaximumFileSizeApplicationLog", "SpecifyMaximumFileSizeSecurityLog", "SpecifyMaximumFileSizeSystemLog", "Channel_LogMaxSize_3" )
# Go through all the special cases that require additional handling
If ($Source -eq "Intune" -and $Finding.Method -eq 'Registry' -and $Finding.RegistryItemIntune -eq "AttackSurfaceReductionRules") {
#
# ASR rules
#
If ($Finding.RegistryItem -eq "ExploitGuard_ASR_Rules") {
# There is no Intune setting for generally enabled ASR rules, if something is configured, HardeningKitty assumes it is in use
If ($Result.Length -gt 0) {
$Result = 1
}
} Else {
# Going through ASR rules
try {
$ResultAsr = $Result.Split("|")
ForEach ($AsrRow in $ResultAsr) {
$AsrRule = $AsrRow.Split("=")
If ($AsrRule[0] -eq $Finding.RegistryItem) {
$Result = $AsrRule[1]
Break
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
}
} ElseIf ($Source -eq "Intune" -and $Finding.Method -eq 'Registry' -and $Finding.RegistryItemIntune -eq "HardenedUNCPaths") {
#
# Hardened UNC path
# Intune uses as a delimiter to configure multiple paths
# All UNC paths are in the value parameter of the "Pol_HardenedPaths" data id
#
$Delimiter = [char]::ConvertFromUtf32(0xF000)
If ($Result.contains($Delimiter)) {
$PreviousValue = ""
try {
# Finding list contains path and the recommend configuration
$RecommendedValue = $Finding.RecommendedValueIntune.Split(";")
# Convert raw Intune value into XML, and separate the components
$XmlString = "$Result"
$XmlObject = [xml]$XmlString
$UncPaths = $XmlObject.root.data.value.Split($Delimiter)
# The path configuration is in the element before the path in the array
ForEach ($UncPath in $UncPaths) {
If ($UncPath -eq $RecommendedValue[0]) {
$Result = $RecommendedValue[0]+";"+$PreviousValue
Break
}
$PreviousValue = $UncPath
}
} catch {
# If something goes wrong or Intune output is unexpected
$Result = "NotConfigured"
}
}
} ElseIf ($Source -eq "Intune" -and $Finding.RegistryItemIntune -eq "PreventInstallationOfMatchingDeviceSetupClasses") {
#
# Prevent installation of devices using drivers that match an device setup class
#
try {
# Finding list contains id and the recommend configuration
$RecommendedValue = $Finding.RecommendedValueIntune.Split(";")
# Check if setting is enabled
If ($RecommendedValue[0] -eq "PreventInstallationOfMatchingDeviceSetupClasses") {
If ($Result.StartsWith(""
} Else {
$Result = $RecommendedValue[0]+";"
}
# Go through additional configuration
} Else {
# Convert raw Intune value to XML
$XmlString = "$Result"
$XmlObject = [xml]$XmlString
ForEach ($row in $XmlObject.root) {
# Check if retroactive mode is active
If ($RecommendedValue[0] -eq "DeviceInstall_Classes_Deny_Retroactive") {
If ($row.data.id -eq $RecommendedValue[0]) {
If ($row.data.value -eq $RecommendedValue[1]) {
$Result = $RecommendedValue[0]+";true"
Break
}
}
# Check list of device classes
} ElseIf ($RecommendedValue[0] -eq "DeviceInstall_Classes_Deny_List") {
If ($row.data.id -eq $RecommendedValue[0]) {
# Intune uses as a delimiter to configure multiple classes
$Delimiter = [char]::ConvertFromUtf32(0xF000)
$DenyList = $row.data.value.Split($Delimiter)
# Go through list of device classes
ForEach ($entry in $DenyList) {
If ($entry -eq $RecommendedValue[1]) {
$Result = $RecommendedValue[0]+";"+$entry
Break
} Else {
$Result = $RecommendedValue[0]+";{NotFound}"
}
}
}
}
}
}
} catch {
# If something goes wrong or Intune output is unexpected
$Result = "NotConfigured"
}
} ElseIf ($Source -eq "Intune" -and $Finding.Method -eq 'Registry' -and $Finding.RegistryItemIntune -in $RegistryItemCollection) {
#
# Parse the Intune settings in "XML" format to retrieve a specific item/configuration
#
try {
$XmlString = "$Result"
$XmlObject = [xml]$XmlString
ForEach ($row in $XmlObject.root.data) {
If ([string]$row.id.Equals($Finding.MethodArgument)) {
$Result = $row.value
Break
}
}
} catch {
# If something goes wrong or Intune output is unexpected
$Result = "NotConfigured"
}
}
#
# Compare the result with the recommendation
#
$ResultPassed = $false
If ($Source -eq "Intune" -and ($Finding.Method -eq "Registry" -or $Finding.Method -eq "RegistryList")) {
Switch ($Finding.OperatorIntune) {
"=" { If ([string] $Result -eq $Finding.RecommendedValueIntune) { $ResultPassed = $true }; Break }
"<=" { try { If ([int]$Result -le [int]$Finding.RecommendedValueIntune) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
"<=!0" { try { If ([int]$Result -le [int]$Finding.RecommendedValueIntune -and [int]$Result -ne 0) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
">=" { try { If ([int]$Result -ge [int]$Finding.RecommendedValueIntune) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
"contains" { If ($Result.ToString().Contains($Finding.RecommendedValueIntune)) { $ResultPassed = $true }; Break }
"notcontains" { If (-not($Result.ToString().Contains($Finding.RecommendedValueIntune))) { $ResultPassed = $true }; Break }
"!=" { If ([string] $Result -ne $Finding.RecommendedValueIntune) { $ResultPassed = $true }; Break }
"=|0" { try { If ([string]$Result -eq $Finding.RecommendedValueIntune -or $Result.Length -eq 0) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
}
} Else {
Switch ($Finding.Operator) {
"=" { If ([string] $Result -eq $Finding.RecommendedValue) { $ResultPassed = $true }; Break }
"<=" { try { If ([int]$Result -le [int]$Finding.RecommendedValue) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
"<=!0" { try { If ([int]$Result -le [int]$Finding.RecommendedValue -and [int]$Result -ne 0) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
">=" { try { If ([int]$Result -ge [int]$Finding.RecommendedValue) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
"contains" { If ($Result.ToString().Contains($Finding.RecommendedValue)) { $ResultPassed = $true }; Break }
"notcontains" { If (-not($Result.ToString().Contains($Finding.RecommendedValue))) { $ResultPassed = $true }; Break }
"!=" { If ([string] $Result -ne $Finding.RecommendedValue) { $ResultPassed = $true }; Break }
"=|0" { try { If ([string]$Result -eq $Finding.RecommendedValue -or $Result.Length -eq 0) { $ResultPassed = $true } } catch { $ResultPassed = $false }; Break }
}
}
#
# Restore Result after SID translation
# The results are already available as SID, for better readability they are translated into their names
#
If ($Finding.Method -eq 'accesschk') {
If ($Result -ne "") {
$ListResult = $Result.Split(";")
ForEach ($AccountSid in $ListResult) {
$AccountName = Get-AccountFromSid -AccountSid $AccountSid
[String] $ResultName += $AccountName.Trim() + ";"
}
$ResultName = $ResultName -replace ".$"
$Result = $ResultName
Clear-Variable -Name ("ResultName")
}
$Finding.RecommendedValue = $SaveRecommendedValue
}
If ($ResultPassed) {
# Passed
$TestResult = "Passed"
If ($Source -eq "Intune" -and ($Finding.Method -eq "Registry" -or $Finding.Method -eq "RegistryList")) {
$MessageRecommendedValue = $Finding.RecommendedValueIntune
} Else {
$MessageRecommendedValue = $Finding.RecommendedValue
}
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", Result=$Result, Recommended=" + $MessageRecommendedValue + ", Severity=Passed"
Write-ResultEntry -Text $Message -SeverityLevel "Passed"
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = "Passed"
Result = $Result
Recommended = $MessageRecommendedValue
TestResult = $TestResult
SeverityFinding = $Finding.Severity
DefaultValue = $ResultDefaultValue
Filter = $Finding.Filter
}
$ReportAllResults += $ReportResult
}
# Increment Counter
$StatsPassed++
} Else {
# Failed
$TestResult = "Failed"
If ($Source -eq "Intune" -and $Finding.Method -eq "Registry") {
$MessageRecommendedValue = $Finding.RecommendedValueIntune
$FindingOperator = $Finding.OperatorIntune
} Else {
$MessageRecommendedValue = $Finding.RecommendedValue
$FindingOperator = $Finding.Operator
}
If ($FindingOperator -eq "!=") {
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", Result=$Result, Recommended=Not " + $MessageRecommendedValue + ", Severity=" + $Finding.Severity
} Else {
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", Result=$Result, Recommended=" + $MessageRecommendedValue + ", Severity=" + $Finding.Severity
}
Write-ResultEntry -Text $Message -SeverityLevel $Finding.Severity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $Finding.Severity
Result = $Result
Recommended = $MessageRecommendedValue
TestResult = $TestResult
SeverityFinding = $Finding.Severity
DefaultValue = $ResultDefaultValue
Filter = $Finding.Filter
}
$ReportAllResults += $ReportResult
}
# Increment Counter
Switch ($Finding.Severity) {
"Low" { $StatsLow++; Break }
"Medium" { $StatsMedium++; Break }
"High" { $StatsHigh++; Break }
}
}
#
# Only return received value
#
} Elseif ($Mode -eq "Config") {
$Message = "ID " + $Finding.ID + "; " + $Finding.Name + "; Result=$Result"
Write-ResultEntry -Text $Message
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = ""
Result = $Result
Recommended = ""
TestResult = ""
SeverityFinding = ""
DefaultValue = $ResultDefaultValue
Filter = $Finding.Filter
}
$ReportAllResults += $ReportResult
}
If ($Backup) {
# Do not save Firewall rules in the backup file, if they are not set
If ( $Finding.Method -eq "FirewallRule" -and !$Result ) {
Continue
}
$BackupResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Method = $Finding.Method
MethodArgument = $Finding.MethodArgument
RegistryPath = $Finding.RegistryPath
RegistryItem = $Finding.RegistryItem
ClassName =$Finding.ClassName
Namespace = $Finding.Namespace
Property = $Finding.Property
DefaultValue = $Finding.DefaultValue
RecommendedValue = $Result
Operator = $Finding.Operator
Severity = $Finding.Severity
}
$BackupAllResults += $BackupResult
}
}
}
}
#
# Start HailMary mode
# HardeningKitty configures all settings in a finding list file.
# Even though HardeningKitty works very carefully, please only
# use HailyMary if you know what you are doing.
#
Elseif ($Mode -eq "HailMary") {
# A CSV finding list is imported
If ($FileFindingList.Length -eq 0) {
# No fallback to a default list anymore, just show an error message
# $CurrentLocation = $PSScriptRoot
# $DefaultList = "$CurrentLocation\lists\finding_list_0x6d69636b_machine.csv"
$Message = "No finding list has been specified - I'm sorry Dave, I'm afraid I can't do that. Please select a suitable list and specify it with the FileFindingList parameter. Select the finding list wisely and check beforehand whether the settings can affect the stability or the function of your system."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Continue
If (Test-Path -Path $DefaultList) {
$FileFindingList = $DefaultList
} Else {
$Message = "The finding list $DefaultList was not found."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Continue
}
}
$FindingList = Import-Csv -Path $FileFindingList -Delimiter ","
# Verify the finding list against the signed official manifest before applying anything.
# HailMary is a write mode: refuse an unverified (custom / modified) list unless the operator
# explicitly accepts the risk with -AllowCustomList.
$ListStatus = Confirm-FindingListIntegrity -ListPath $FileFindingList `
-ManifestPath (Join-Path -Path $PSScriptRoot -ChildPath "lists\$HardeningKittyListManifestName") `
-SignaturePath (Join-Path -Path $PSScriptRoot -ChildPath "lists\$HardeningKittyListManifestName.p7s") `
-PinnedThumbprint $HardeningKittyListSigningThumbprint
If ($ListStatus.Verified) {
Write-ProtocolEntry -Text "Finding list verified as official (signature valid). SHA-256: $($ListStatus.Hash)" -LogLevel "Success"
} ElseIf ($AllowCustomList) {
Write-ProtocolEntry -Text "Applying an UNVERIFIED finding list at your own risk ($($ListStatus.Reason)). SHA-256: $($ListStatus.Hash)" -LogLevel "Warning"
} Else {
Write-ProtocolEntry -Text "Refusing to apply an unverified finding list in $Mode mode ($($ListStatus.Reason)). SHA-256: $($ListStatus.Hash). Re-run with -AllowCustomList to apply it at your own risk." -LogLevel "Error"
Break
}
If ($Filter) {
$FindingList = $FindingList | Where-Object -FilterScript $Filter
If ($FindingList.Length -eq 0) {
$Message = "Your filter did not return any results, please adjust the filter so that HardeningKitty has something to work with."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
}
$LastCategory = ""
$ProcessmitigationEnableArray = @()
$ProcessmitigationDisableArray = @()
#
# Stop before changing anything if admin rights are missing
# Without admin rights these findings would be skipped one by one, leaving the system
# partially hardened (issue #206). HKCU registry findings, e.g. a user list applied by
# the user themselves, work without admin rights.
#
If (-not($IsAdmin)) {
$AdminMethods = @('secedit', 'auditpol', 'accountpolicy', 'accesschk', 'WindowsOptionalFeature', 'MpPreference', 'MpPreferenceAsr', 'Processmitigation', 'bcdedit', 'FirewallRule', 'ScheduledTask')
$AdminFindings = @($FindingList | Where-Object {
($AdminMethods -contains $_.Method) -or
(($_.Method -eq 'Registry' -or $_.Method -eq 'RegistryList') -and -not($_.RegistryPath.StartsWith("HKCU:\")))
})
If ($AdminFindings.Count -gt 0) {
$Message = "HailMary requires admin privileges for $($AdminFindings.Count) finding(s) in this list. Nothing has been changed. Please run HardeningKitty as administrator."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
If ($HardeningKittyTempDir -and (Test-Path -LiteralPath $HardeningKittyTempDir)) {
Remove-Item -LiteralPath $HardeningKittyTempDir -Recurse -Force -ErrorAction SilentlyContinue
}
Return
}
}
#
# Create a System Restore Point
#
If (-not($SkipRestorePoint)) {
$Message = "Creating a system restore point"
Write-Output "`n"
Write-ProtocolEntry -Text $Message -LogLevel "Info"
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID "42" -FindingName "System Restore Point" -FindingMethod "Checkpoint-Computer"
Continue
}
If ($PSVersionTable.PSVersion.Major -gt 5) {
$Message = "The cmdlet used to create a system restore has been removed from PowerShell in version 6 onwards. To create a system restore, run HardeninKitty with PowerShell version 5. Alternatively, create a system restore point manually and use the -SkipRestorePoint parameter to run HailMary anyway. Be careful!"
Write-ResultEntry -Text $Message -SeverityLevel "High"
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
Break
}
Try {
Enable-ComputerRestore -Drive $Env:SystemDrive
Checkpoint-Computer -Description 'HardeningKitty' -RestorePointType 'MODIFY_SETTINGS' -ErrorAction Stop -WarningAction Stop
} catch {
$Message = "Creating a system restore point failed. Use -SkipRestorePoint to run HailMary anyway. Be careful!"
Write-ResultEntry -Text $Message -SeverityLevel "High"
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
Break
}
$Message = "Creating a system restore point was successful"
Write-ResultEntry -Text $Message -SeverityLevel "Passed"
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
}
ForEach ($Finding in $FindingList) {
#
# Category
#
If ($LastCategory -ne $Finding.Category) {
$Message = "Starting Category " + $Finding.Category
Write-Output "`n"
Write-ProtocolEntry -Text $Message -LogLevel "Info"
$LastCategory = $Finding.Category
}
#
# Registry
# Create or modify a registry value.
#
If ($Finding.Method -eq 'Registry' -or $Finding.Method -eq 'RegistryList') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin) -and -not($Finding.RegistryPath.StartsWith("HKCU:\"))) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
#
# Do not set/configure certain registry
# ASR rules configured with Intune (ASRRules, ASROnlyExclusions)
# Defender expections configured with Intune (ExcludedExtensions, ExcludedPaths, ExcludedProcesses)
#
If ($Finding.RegistryItem -eq "ASRRules" -Or $Finding.RegistryItem -eq "ASROnlyExclusions" -Or $Finding.RegistryItem -eq "ExcludedExtensions" -Or $Finding.RegistryItem -eq "ExcludedPaths" -Or $Finding.RegistryItem -eq "ExcludedProcesses") {
$ResultText = "This setting is not configured by HardeningKitty"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Continue
}
$RegType = "String"
#
# Basically this is true, but there is an exception for the finding "MitigationOptions_FontBocking",
# the value "10000000000" is written to the registry as a string...
#
# ... and more exceptions are added over time:
#
# MitigationOptions_FontBocking => Mitigation Options: Untrusted Font Blocking
# Machine => Network access: Remotely accessible registry paths
# Retention => Event Log Service: *: Control Event Log behavior when the log file reaches its maximum size
# AllocateDASD => Devices: Allowed to format and eject removable media
# ScRemoveOption => Interactive logon: Smart card removal behavior
# AutoAdminLogon => MSS: (AutoAdminLogon) Enable Automatic Logon (not recommended)
#
If ($Finding.RegistryItem -eq "MitigationOptions_FontBocking" -Or $Finding.RegistryItem -eq "Retention" -Or $Finding.RegistryItem -eq "AllocateDASD" -Or $Finding.RegistryItem -eq "ScRemoveOption" -Or $Finding.RegistryItem -eq "AutoAdminLogon") {
$RegType = "String"
} ElseIf ($Finding.RegistryItem -eq "Machine" -Or $Finding.RegistryItem -eq "EccCurves" -Or $Finding.RegistryItem -eq "NullSessionPipes" -Or $Finding.RegistryItem -eq "NullSessionShares") {
$RegType = "MultiString"
$Finding.RecommendedValue = $Finding.RecommendedValue -split ";"
} ElseIf ($Finding.RecommendedValue -match "^\d+$") {
$RegType = "DWord"
}
If (!(Test-Path $Finding.RegistryPath)) {
$Result = New-Item $Finding.RegistryPath -Force;
If ($Result) {
$ResultText = "Registry key created"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
} Else {
$ResultText = "Failed to create registry key"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Continue
}
}
#
# The method RegistryList needs a separate handling, because the name of the registry key is dynamic, usually incremented.
# Therefore, it is searched whether the value already exists or not. If the value does not exist, it counts how many
# other values are already there in order to set the next higher value and not overwrite existing keys.
#
If ($Finding.Method -eq 'RegistryList') {
$RegistryItemFound = $false
$ListPolicies = $Finding.RegistryPath
$ResultList = Get-ItemProperty -Path $Finding.RegistryPath
$ResultListCounter = 0
If ($ResultList | Where-Object { $_ -like "*" + $Finding.RegistryItem + "*" }) {
$ResultList.PSObject.Properties | ForEach-Object {
If ($_.Value -eq $Finding.RegistryItem) {
$Finding.RegistryItem = $_.Name
$RegistryItemFound = $true
}
}
} Else {
$ResultList.PSObject.Properties | ForEach-Object {
$ResultListCounter++
}
}
# Check if registryItem (key name) has been found or not
If ($RegistryItemFound -eq $false) {
If ($ResultListCounter -eq 0) {
$Finding.RegistryItem = 1
} Else {
# Check if key is already used and can be used
$KeyAlreadyExists = $true
$Finding.RegistryItem = 1
while ($KeyAlreadyExists){
try {
# This key exists and should be incremented
$Result = Get-ItemPropertyValue -Path $Finding.RegistryPath -Name $Finding.RegistryItem
$Finding.RegistryItem=$Finding.RegistryItem+1
$KeyAlreadyExists = $true;
} catch {
# This key does not exist and it can be used
$KeyAlreadyExists = $false;
}
}
}
}
}
$ResultText = ""
# Remove this policy if it should not exists
If ($Finding.RecommendedValue -eq '-NODATA-') {
# Check if the key (item) already exists
$keyExists = $true;
try {
# This key exists
$Result = Get-ItemPropertyValue -Path $Finding.RegistryPath -Name $Finding.RegistryItem
} catch {
# This key does not exist
$keyExists = $false;
}
If ($keyExists) {
# key exists
try {
Remove-ItemProperty -Path $Finding.RegistryPath -Name $Finding.RegistryItem
$ResultText = "Registry key removed"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} catch {
$ResultText = "Failed to remove registry key"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
}
} Else {
# key does not exists
If ($Finding.Method -eq 'RegistryList') {
# Don't show incorrect item
$ResultText = "This value does not already exists in list policy"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $ResultText
} Else {
$ResultText = "This key policy does not already exists"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
}
$MessageSeverity = "Low"
$TestResult = "Passed"
}
} Else {
$Result = Set-ItemProperty -PassThru -Path $Finding.RegistryPath -Name $Finding.RegistryItem -Type $RegType -Value $Finding.RecommendedValue
if ($Result) {
$ResultText = "Registry value created/modified"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} else {
$ResultText = "Failed to create registry value"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
}
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# secedit
# Set a security policy
#
If ($Finding.Method -eq 'secedit') {
# Check if Secedit binary is available, skip test if not
If (-Not (Test-Path $BinarySecedit)) {
Write-BinaryError -Binary $BinarySecedit -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$Area = "";
Switch ($Finding.Category) {
"Account Policies" { $Area = "SECURITYPOLICY"; Break }
"Security Options" { $Area = "SECURITYPOLICY"; Break }
}
# MethodArgument comes from the finding list and is written into the security
# policy that is applied with secedit /configure /overwrite. Reject control
# characters (CR/LF) so a finding list entry cannot inject additional INI lines.
If ($Finding.MethodArgument -match "[\r\n]") {
$ResultText = "Invalid MethodArgument, contains control characters"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Continue
}
$TempFileName = New-HardeningKittyTempFile -Directory $HardeningKittyTempDir -CreateFile
# secedit.exe must create the database itself: a pre-created empty file fails with
# "Error 9: The storage control block address is invalid" (issue #214)
$TempDbFileName = New-HardeningKittyTempFile -Directory $HardeningKittyTempDir -Extension ".sdb"
&$BinarySecedit /export /cfg $TempFileName /areas $Area | Out-Null
$Data = Get-IniContent $TempFileName
Set-HashtableValueDeep -Table $Data -Path $Finding.MethodArgument -Value $Finding.RecommendedValue
Out-IniFile -InputObject $Data -FilePath $TempFileName -Encoding Unicode
&$BinarySecedit /configure /cfg $TempFileName /overwrite /areas $Area /db $TempDbFileName /quiet | Out-Null
if ($LastExitCode -ne 0) {
$ResultText = "Failed to configure security policy"
$Message = "ID " + $Finding.ID + ", " + $Finding.MethodArgument + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Remove-Item $TempFileName
Remove-Item $TempDbFileName -ErrorAction SilentlyContinue
Continue
}
$ResultText = "Configured security policy"
$Message = "ID " + $Finding.ID + ", " + $Finding.MethodArgument + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Remove-Item $TempFileName
Remove-Item $TempDbFileName -ErrorAction SilentlyContinue
}
#
# auditpol
# Set an audit policy
#
If ($Finding.Method -eq 'auditpol') {
# Check if Auditpol binary is available, skip test if not
If (-Not (Test-Path $BinaryAuditpol)) {
Write-BinaryError -Binary $BinaryAuditpol -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$Success = if ($Finding.RecommendedValue -ilike "*success*") { "enable" } else { "disable" }
$Failure = if ($Finding.RecommendedValue -ilike "*failure*") { "enable" } else { "disable" }
$SubCategory = $Finding.MethodArgument
&$BinaryAuditpol /set /subcategory:"$($SubCategory)" /success:$($Success) /failure:$($Failure) | Out-Null
if ($LastExitCode -eq 0) {
$ResultText = "Audit policy set"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} else {
$ResultText = "Failed to set audit policy"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# accountpolicy
# Set a user account policy
#
If ($Finding.Method -eq 'accountpolicy') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if net binary is available, skip test if not
If (-Not (Test-Path $BinaryNet)) {
Write-BinaryError -Binary $BinaryNet -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$Sw = "";
Switch ($Finding.Name) {
"Force user logoff how long after time expires" { $Sw = "/FORCELOGOFF:$($Finding.RecommendedValue)"; Break }
"Minimum password age" { $Sw = "/MINPWAGE:$($Finding.RecommendedValue)"; Break }
"Maximum password age" { $Sw = "/MAXPWAGE:$($Finding.RecommendedValue)"; Break }
"Minimum password length" { $Sw = "/MINPWLEN:$($Finding.RecommendedValue)"; Break }
"Length of password history maintained" { $Sw = "/UNIQUEPW:$($Finding.RecommendedValue)"; Break }
"Account lockout threshold" { $Sw = "/lockoutthreshold:$($Finding.RecommendedValue)"; Break; }
"Account lockout duration" { $Sw = @("/lockoutwindow:$($Finding.RecommendedValue)", "/lockoutduration:$($Finding.RecommendedValue)"); Break }
"Reset account lockout counter" { $Sw = "/lockoutwindow:$($Finding.RecommendedValue)"; Break }
}
&$BinaryNet accounts $Sw | Out-Null
if ($LastExitCode -eq 0) {
$ResultText = "Account policy set"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} else {
$ResultText = "Failed to set account policy"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# accesschk
# For the audit mode, accesschk is used, but the rights are set with secedit.
#
If ($Finding.Method -eq 'accesschk') {
# Check if Secedit binary is available, skip test if not
If (-Not (Test-Path $BinarySecedit)) {
Write-BinaryError -Binary $BinarySecedit -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$Area = "USER_RIGHTS";
# MethodArgument comes from the finding list and is written into the security
# policy that is applied with secedit /configure /overwrite. Reject control
# characters (CR/LF) so a finding list entry cannot inject additional INI lines.
If ($Finding.MethodArgument -match "[\r\n]") {
$ResultText = "Invalid MethodArgument, contains control characters"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Continue
}
$TempFileName = New-HardeningKittyTempFile -Directory $HardeningKittyTempDir -CreateFile
# secedit.exe must create the database itself: a pre-created empty file fails with
# "Error 9: The storage control block address is invalid" (issue #214)
$TempDbFileName = New-HardeningKittyTempFile -Directory $HardeningKittyTempDir -Extension ".sdb"
&$BinarySecedit /export /cfg $TempFileName /areas $Area | Out-Null
# MethodArgument is used to locate and rewrite the matching line. Escape it so it is
# treated as literal text and cannot inject regular-expression metacharacters. The
# replacement text is likewise escaped so a literal "$" is not interpreted as a
# regex substitution token ($1, $&, ...).
$MethodArgumentPattern = [regex]::Escape($Finding.MethodArgument) + ".*"
if ($Finding.RecommendedValue -eq "") {
$Replacement = ($Finding.MethodArgument + " = ").Replace('$', '$$')
(Get-Content -Encoding unicode $TempFileName) -replace $MethodArgumentPattern, $Replacement | Out-File $TempFileName
} else {
$ListTranslated = @()
$Finding.RecommendedValue -split ';' | Where-Object {
# Get SID to translate the account name
$AccountSid = Translate-SidFromWellkownAccount -AccountName $_
# Get account name from system with SID (local translation)
$AccountName = Get-AccountFromSid -AccountSid $AccountSid
$ListTranslated += $AccountName
}
# If User Right Assignment exists, replace values
If ( ((Get-Content -Encoding unicode $TempFileName) | Select-String -SimpleMatch $Finding.MethodArgument).Count -gt 0 ) {
$Replacement = ($Finding.MethodArgument + " = " + ($ListTranslated -join ',')).Replace('$', '$$')
(Get-Content -Encoding unicode $TempFileName) -replace $MethodArgumentPattern, $Replacement | Out-File $TempFileName
}
# If it does not exist, add a new entry into the file at the right position
Else {
$TempFileContent = Get-Content -Encoding unicode $TempFileName
$LineNumber = $TempFileContent.Count
$TempFileContent[$LineNumber - 3] = "$($Finding.MethodArgument) = $($ListTranslated -join ',')"
$TempFileContent[$LineNumber - 2] = "[Version]"
$TempFileContent[$LineNumber - 1] = 'signature="$CHICAGO$"'
$TempFileContent += "Revision=1"
$TempFileContent | Set-Content -Encoding unicode $TempFileName
}
}
&$BinarySecedit /configure /cfg $TempFileName /overwrite /areas $Area /db $TempDbFileName /quiet | Out-Null
if ($LastExitCode -ne 0) {
$ResultText = "Failed to configure system user right assignment"
$Message = "ID " + $Finding.ID + ", " + $Finding.MethodArgument + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Remove-Item $TempFileName
Remove-Item $TempDbFileName -ErrorAction SilentlyContinue
Continue
}
$ResultText = "Configured system user right assignment"
$Message = "ID " + $Finding.ID + ", " + $Finding.MethodArgument + ", " + $Finding.RecommendedValue + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Remove-Item $TempFileName
Remove-Item $TempDbFileName -ErrorAction SilentlyContinue
}
#
# WindowsOptionalFeature
# Install / Remove a Windows feature
#
If ($Finding.Method -eq 'WindowsOptionalFeature') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
#
# Check if feature is installed and should be removed, or
# it is missing and should be installed
#
try {
$ResultOutput = Get-WindowsOptionalFeature -Online -FeatureName $Finding.MethodArgument
$Result = $ResultOutput.State
} catch {
$ResultText = "Could not check status"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Continue
}
# Feature will be removed, a reboot will be suppressed
If ($Result -eq "Enabled" -and $Finding.RecommendedValue -eq "Disabled") {
try {
$Result = Disable-WindowsOptionalFeature -NoRestart -Online -FeatureName $Finding.MethodArgument
} catch {
$ResultText = "Could not be removed"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Continue
}
$ResultText = "Feature removed"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
}
# No changes required
ElseIf ($Result -eq "Disabled" -and $Finding.RecommendedValue -eq "Disabled") {
$ResultText = "Feature is not installed"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
}
# Feature will be installed, a reboot will be suppressed
ElseIf ($Result -eq "Disabled" -and $Finding.RecommendedValue -eq "Enabled") {
try {
$Result = Enable-WindowsOptionalFeature -NoRestart -Online -FeatureName $Finding.MethodArgument
} catch {
$ResultText = "Could not be installed"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
Continue
}
$ResultText = "Feature installed"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
}
# No changes required
ElseIf ($Result -eq "Enabled" -and $Finding.RecommendedValue -eq "Enabled") {
$ResultText = "Feature is already installed"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# MpPreference
# Set a Windows Defender policy
#
If ($Finding.Method -eq 'MpPreference') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$ResultMethodArgument = $Finding.MethodArgument
$ResultRecommendedValue = $Finding.RecommendedValue
Switch ($ResultRecommendedValue) {
"True" { $ResultRecommendedValue = 1; Break }
"False" { $ResultRecommendedValue = 0; Break }
}
# Build a hashtable MpPreferenceArgs for splatting arguments to Set-MpPreference. See https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting
$MpPreferenceArgs = @{
$ResultMethodArgument = $ResultRecommendedValue
}
Set-MpPreference @MpPreferenceArgs
if ($?) {
$ResultText = "Method value modified"
$Message = "ID " + $Finding.ID + ", " + $Finding.MethodArgument + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} else {
$ResultText = "Failed to change Method value"
$Message = "ID " + $Finding.ID + ", " + $Finding.MethodArgument + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Passed"
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# Microsoft Defender Preferences - Attack surface reduction rules (ASR rules)
# The values are saved from a PowerShell function into an object.
# The desired arguments can be accessed directly.
#
If ($Finding.Method -eq 'MpPreferenceAsr') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$ResultMethodArgument = $Finding.MethodArgument
$ResultRecommendedValue = $Finding.RecommendedValue
Switch ($ResultRecommendedValue) {
"True" { $ResultRecommendedValue = 1; Break }
"False" { $ResultRecommendedValue = 0; Break }
}
# Build a hashtable MpPreferenceArgs for splatting arguments to Set-MpPreference. See https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting
$MpPreferenceArgs = @{
AttackSurfaceReductionRules_Ids = $ResultMethodArgument
AttackSurfaceReductionRules_Actions = $ResultRecommendedValue
}
Add-MpPreference @MpPreferenceArgs
if ($?) {
$ResultText = "ASR rule added to list"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $Finding.MethodArgument + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} else {
$ResultText = "Failed to add ASR rule"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $Finding.MethodArgument + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# Exploit protection
# Set exploit protection values
#
# I noticed irregularities when the process mitigations were set individually,
# in some cases settings that had already been set were then reset. Therefore,
# the settings are collected in an array and finally set at the end of the processing.
#
If ($Finding.Method -eq 'Processmitigation') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$SettingArgumentArray = $Finding.MethodArgument.Split(".")
If ( $Finding.RecommendedValue -eq "ON") {
If ( $SettingArgumentArray[1] -eq "Enable" ) {
$ProcessmitigationEnableArray += $SettingArgumentArray[0]
} Else {
$ProcessmitigationEnableArray += $SettingArgumentArray[1]
}
} ElseIf ( $Finding.RecommendedValue -eq "OFF") {
If ($SettingArgumentArray[1] -eq "TelemetryOnly") {
$ProcessmitigationDisableArray += "SEHOPTelemetry"
} ElseIf ( $SettingArgumentArray[1] -eq "Enable" ) {
$ProcessmitigationDisableArray += $SettingArgumentArray[0]
} Else {
$ProcessmitigationDisableArray += $SettingArgumentArray[1]
}
}
$ResultText = "setting added to list"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# bcdedit
# Force use of Data Execution Prevention, if it is not already set
#
If ($Finding.Method -eq 'bcdedit') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check if Bcdedit binary is available, skip test if not
If (-Not (Test-Path $BinaryBcdedit)) {
Write-BinaryError -Binary $BinaryBcdedit -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
try {
$ResultOutput = &$BinaryBcdedit
$ResultOutput = $ResultOutput | Where-Object { $_ -like "*" + $Finding.RecommendedValue + "*" }
If ($ResultOutput -match ' ([a-z,A-Z]+)') {
$Result = $Matches[1]
} Else {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
If ($Result -ne $Finding.RecommendedValue) {
try {
$ResultOutput = &$BinaryBcdedit "/set" $Finding.MethodArgument $Finding.RecommendedValue
$ResultExitCode = $LASTEXITCODE
} catch {
$ResultExitCode = 1
}
If ($ResultExitCode -eq 0) {
$ResultText = "Setting enabled. Please restart the system to activate it"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} Else {
$ResultText = "Setting could not be enabled"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
}
} Else {
$ResultText = "Setting is already set correct"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# FirewallRule
# Create a firewall rule. First it will be checked if the rule already exists
#
If ($Finding.Method -eq 'FirewallRule') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
$FwState = $Finding.RecommendedValue
If ($FwState -eq 'False') {
# Do not create a firewall rule with state equal to false
Continue
}
$FwRule = $Finding.MethodArgument
$FwRuleArray = $FwRule.Split("|")
$FwDisplayName = $Finding.Name
$FwProfile = $FwRuleArray[0]
$FwDirection = $FwRuleArray[1]
$FwAction = $FwRuleArray[2]
$FwProtocol = $FwRuleArray[3]
$FwLocalPort = @($FwRuleArray[4]).Split(",")
$FwProgram = $FwRuleArray[5]
# Check if rule already exists
try {
$ResultOutput = Get-NetFirewallRule -PolicyStore ActiveStore -DisplayName $FwDisplayName 2> $null
$Result = $ResultOutput.Enabled
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
# Go on if rule not exists
If (-Not $Result) {
If ($FwProgram -eq "") {
$ResultRule = New-NetFirewallRule -DisplayName $FwDisplayName -Profile $FwProfile -Direction $FwDirection -Action $FwAction -Protocol $FwProtocol -LocalPort $FwLocalPort
} Else {
$ResultRule = New-NetFirewallRule -DisplayName $FwDisplayName -Profile $FwProfile -Direction $FwDirection -Action $FwAction -Program "$FwProgram"
}
If ($ResultRule.PrimaryStatus -eq "OK") {
# Excellent
$ResultText = "Rule created"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} Else {
# Bogus
$ResultText = "Rule not created"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
}
} Else {
# Excellent
$ResultText = "Rule already exists, setting Recommended Value"
Set-NetFirewallRule -DisplayName $FwDisplayName -Enabled $Finding.RecommendedValue
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
#
# Scheduled Task
# Edit a scheduled task. First it will be checked if a modification is required
#
If ($Finding.Method -eq 'ScheduledTask') {
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID $Finding.ID -FindingName $Finding.Name -FindingMethod $Finding.Method
Continue
}
# Check the state of the scheduled task
try {
$ResultOutput = Get-ScheduledTask -TaskName $Finding.MethodArgument 2> $null
$Result = $ResultOutput.State
} catch {
$Result = $Finding.DefaultValue
$ResultDefaultValue = $true
}
# Check if a modification is requried
If ($Result -eq $Finding.RecommendedValue) {
# Excellent
$ResultText = "Scheduled Task has alredy the recommended state"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} Else {
If ($Finding.RecommendedValue -eq "Disabled") {
$Result = Get-ScheduledTask -TaskName $Finding.MethodArgument | Disable-ScheduledTask
$ResultText = "Scheduled Task was disabled"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} ElseIf ($Finding.RecommendedValue -eq "Ready") {
$Result = Get-ScheduledTask -TaskName $Finding.MethodArgument | Enable-ScheduledTask
$ResultText = "Scheduled Task was enabled"
$Message = "ID " + $Finding.ID + ", " + $Finding.Name + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
}
}
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
}
#
# After all items of the checklist have been run through, the process mitigation settings can now be set...
#
If ( $ProcessmitigationEnableArray.Count -gt 0 -and $ProcessmitigationDisableArray.Count -gt 0) {
$ResultText = "Process mitigation settings set"
$MessageSeverity = "Passed"
$TestResult = "Passed"
try {
$Result = Set-ProcessMitigation -System -Enable $ProcessmitigationEnableArray -Disable $ProcessmitigationDisableArray
} catch {
$ResultText = "Failed to set process mitigation settings"
$MessageSeverity = "High"
$TestResult = "Failed"
}
$Message = "Starting Category Microsoft Defender Exploit Guard"
Write-Output "`n"
Write-ProtocolEntry -Text $Message -LogLevel "Info"
$Message = $ResultText
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
} ElseIf ($ProcessmitigationEnableArray.Count -gt 0 -and $ProcessmitigationDisableArray.Count -eq 0) {
$ResultText = "Process mitigation settings set"
$MessageSeverity = "Passed"
$TestResult = "Passed"
try {
$Result = Set-ProcessMitigation -System -Enable $ProcessmitigationEnableArray
} catch {
$ResultText = "Failed to set process mitigation settings"
$MessageSeverity = "High"
$TestResult = "Failed"
}
$Message = "Starting Category Microsoft Defender Exploit Guard"
Write-Output "`n"
Write-ProtocolEntry -Text $Message -LogLevel "Info"
$Message = $ResultText
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
} ElseIf ($ProcessmitigationEnableArray.Count -eq 0 -and $ProcessmitigationDisableArray.Count -gt 0) {
$ResultText = "Process mitigation settings set"
$MessageSeverity = "Passed"
$TestResult = "Passed"
try {
$Result = Set-ProcessMitigation -System -Disable $ProcessmitigationDisableArray
} catch {
$ResultText = "Failed to set process mitigation settings"
$MessageSeverity = "High"
$TestResult = "Failed"
}
$Message = "Starting Category Microsoft Defender Exploit Guard"
Write-Output "`n"
Write-ProtocolEntry -Text $Message -LogLevel "Info"
$Message = $ResultText
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
If ($Report) {
$ReportResult = [ordered] @{
ID = $Finding.ID
Category = $Finding.Category
Name = $Finding.Name
Severity = $MessageSeverity
Result = $ResultText
Recommended = ""
TestResult = $TestResult
SeverityFinding = ""
}
$ReportAllResults += $ReportResult
}
}
}
#
# Start GPO mode
# HardeningKitty configures all settings in a finding list file.
# Even though HardeningKitty works very carefully.
# The GPO mode create a GPO containing every registry method remediation.
#
Elseif ($Mode -eq "GPO") {
Write-Output "`n"
If ($GPOname.Length -eq 0) {
# Control if a GPO name is given
$Message = "The GPO Name $GPOname was not found."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
If ($FileFindingList.Length -eq 0) {
# Control if a Finding list is given
$CurrentLocation = $PSScriptRoot
$DefaultList = "$CurrentLocation\lists\finding_list_0x6d69636b_machine.csv"
If (Test-Path -Path $DefaultList) {
$FileFindingList = $DefaultList
} Else {
$Message = "The finding list $DefaultList was not found."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
}
# Check if the user has admin rights, skip test if not
If (-not($IsAdmin)) {
Write-NotAdminError -FindingID "0" -FindingName "GPO Mode" -FindingMethod "Create a GPO"
Continue
}
# Check if the New-GPO cmdlet is available
try {
$CheckRsatStatus = Get-Command New-GPO -ErrorAction Stop
} catch {
Write-BinaryError -Binary "Group Policy Management PowerShell Module" -FindingID "0" -FindingName "GPO Mode" -FindingMethod "Create a GPO"
Continue
}
# Should check if user is domain admin
try {
New-GPO -Name $GPOname -ErrorAction Stop | Out-Null
}
catch [System.ArgumentException] {
# Control if the Name of the GPO is ok
Write-ProtocolEntry -Text $_.Exception.Message -LogLevel "Error"
Break
}
# Iterrate over finding list
$FindingList = Import-Csv -Path $FileFindingList -Delimiter ","
# Verify the finding list against the signed official manifest before creating the GPO.
# GPO is a write mode: refuse an unverified (custom / modified) list unless the operator
# explicitly accepts the risk with -AllowCustomList. This runs before New-GPO so a refused
# run does not leave an empty GPO behind.
$ListStatus = Confirm-FindingListIntegrity -ListPath $FileFindingList `
-ManifestPath (Join-Path -Path $PSScriptRoot -ChildPath "lists\$HardeningKittyListManifestName") `
-SignaturePath (Join-Path -Path $PSScriptRoot -ChildPath "lists\$HardeningKittyListManifestName.p7s") `
-PinnedThumbprint $HardeningKittyListSigningThumbprint
If ($ListStatus.Verified) {
Write-ProtocolEntry -Text "Finding list verified as official (signature valid). SHA-256: $($ListStatus.Hash)" -LogLevel "Success"
} ElseIf ($AllowCustomList) {
Write-ProtocolEntry -Text "Applying an UNVERIFIED finding list at your own risk ($($ListStatus.Reason)). SHA-256: $($ListStatus.Hash)" -LogLevel "Warning"
} Else {
Write-ProtocolEntry -Text "Refusing to apply an unverified finding list in $Mode mode ($($ListStatus.Reason)). SHA-256: $($ListStatus.Hash). Re-run with -AllowCustomList to apply it at your own risk." -LogLevel "Error"
Break
}
If ($Filter) {
$FindingList = $FindingList | Where-Object -FilterScript $Filter
If ($FindingList.Length -eq 0) {
$Message = "Your filter did not return any results, please adjust the filter so that HardeningKitty has something to work with."
Write-ProtocolEntry -Text $Message -LogLevel "Error"
Break
}
}
ForEach ($Finding in $FindingList) {
#
# Only Registry Method Policies
#
If ($Finding.Method -eq "Registry") {
$RegType = "String"
#
# Basically this is true, but there is an exception for the finding "MitigationOptions_FontBocking",
# the value "10000000000" is written to the registry as a string...
#
# ... and more exceptions are added over time:
#
# MitigationOptions_FontBocking => Mitigation Options: Untrusted Font Blocking
# Machine => Network access: Remotely accessible registry paths
# Retention => Event Log Service: *: Control Event Log behavior when the log file reaches its maximum size
# AllocateDASD => Devices: Allowed to format and eject removable media
# ScRemoveOption => Interactive logon: Smart card removal behavior
# AutoAdminLogon => MSS: (AutoAdminLogon) Enable Automatic Logon (not recommended)
#
If ($Finding.RegistryItem -eq "MitigationOptions_FontBocking" -Or $Finding.RegistryItem -eq "Retention" -Or $Finding.RegistryItem -eq "AllocateDASD" -Or $Finding.RegistryItem -eq "ScRemoveOption" -Or $Finding.RegistryItem -eq "AutoAdminLogon") {
$RegType = "String"
} ElseIf ($Finding.RegistryItem -eq "Machine") {
$RegType = "MultiString"
$Finding.RecommendedValue = $Finding.RecommendedValue -split ";"
} ElseIf ($Finding.RecommendedValue -match "^\d+$") {
$RegType = "DWord"
$Finding.RecommendedValue = ConvertToInt -string $Finding.RecommendedValue
}
$RegPath = $Finding.RegistryPath.Replace(":","")
$RegItem = $Finding.RegistryItem
try {
Set-GPRegistryValue -Name $GPOname -Key $RegPath -ValueName $RegItem -Type $RegType -Value $Finding.RecommendedValue | Out-Null
$ResultText = "Registry value added successfully"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $Finding.RegistryItem + ", " + $ResultText
$MessageSeverity = "Passed"
$TestResult = "Passed"
} catch {
$ResultText = "Failed to add registry key"
$Message = "ID " + $Finding.ID + ", " + $Finding.RegistryPath + ", " + $ResultText
$MessageSeverity = "High"
$TestResult = "Failed"
} finally {
Write-ResultEntry -Text $Message -SeverityLevel $MessageSeverity
If ($Log) {
Add-MessageToFile -Text $Message -File $LogFile
}
}
}
}
}
# Write report file
If ($Report) {
Write-Output "`n"
Write-ProtocolEntry -Text "HardeningKitty prepares the report" -LogLevel "Info"
ForEach ($ReportResult in $ReportAllResults) {
$ResultObject = [pscustomobject] $ReportResult
$ResultObject | Export-Csv -Path $ReportFile -Delimiter "," -NoTypeInformation -Append
}
Write-ProtocolEntry -Text "Report done" -LogLevel "Info"
}
# Write backup file
If ($Backup) {
Write-Output "`n"
Write-ProtocolEntry -Text "HardeningKitty prepares the backup file" -LogLevel "Info"
ForEach ($BackupResult in $BackupAllResults) {
$BackupObject = [pscustomobject] $BackupResult
$BackupObject | Export-Csv -Path $BackupFile -Delimiter "," -NoTypeInformation -Append
}
Write-ProtocolEntry -Text "Backup file done" -LogLevel "Info"
}
If ($Mode -eq "Audit") {
# HardeningKitty Score
$StatsTotal = $StatsPassed + $StatsLow + $StatsMedium + $StatsHigh
$ScoreTotal = $StatsTotal * 4
$ScoreAchived = $StatsPassed * 4 + $StatsLow * 2 + $StatsMedium
If ($ScoreTotal -ne 0 ) {
$HardeningKittyScore = ([int] $ScoreAchived / [int] $ScoreTotal) * 5 + 1
}
$HardeningKittyScoreRounded = [math]::round($HardeningKittyScore, 2)
# Overwrite HardeningKitty Score if no finding is passed
If ($StatsPassed -eq 0 ) {
$HardeningKittyScoreRounded = 1.00
}
If ($Script:StatsError -gt 0) {
Write-ProtocolEntry -Text "During the execution of HardeningKitty errors occurred due to missing admin rights or tools. For a complete result, these errors should be resolved. Total errors: $Script:StatsError" -LogLevel "Error"
}
Write-Output "`n"
Write-ProtocolEntry -Text "Your HardeningKitty score is: $HardeningKittyScoreRounded. HardeningKitty Statistics: Total checks: $StatsTotal - Passed: $StatsPassed, Low: $StatsLow, Medium: $StatsMedium, High: $StatsHigh." -LogLevel "Info"
}
# Remove the private working directory and any remaining temporary files
If ($HardeningKittyTempDir -and (Test-Path -LiteralPath $HardeningKittyTempDir)) {
Remove-Item -LiteralPath $HardeningKittyTempDir -Recurse -Force -ErrorAction SilentlyContinue
}
Write-ProtocolEntry -Text "HardeningKitty is done" -LogLevel "Info"
}
Export-ModuleMember -Function Invoke-HardeningKitty