functions/System/Hardening/Invoke-SecurityHardening.ps1
|
function Invoke-SecurityHardening { <# .SYNOPSIS Applies security hardening rules to a system based on hardening profile. .DESCRIPTION Orchestrates the application of hardening rules from a loaded hardening session. Iterates through all rules in the session's profile and applies each rule according to its type (Registry, Service, Firewall, Audit, etc.). Supports dry-run mode via WhatIf and provides detailed logging of all applied, failed, and skipped rules. Returns a comprehensive hardening result object with compliance information. Rules are applied with error handling: individual rule failures do not stop the overall hardening process unless FailOnError is specified. .PARAMETER Session The hardening session object created by New-HardeningSession. Mandatory. Must contain valid Profile, TargetSystem, and Rules. .PARAMETER RuleFilter Optional array of specific rule names to apply. If omitted, applies all rules from the profile. Useful for targeted hardening or remediation. .PARAMETER FailOnError If specified, stops hardening and throws exception on first rule failure. Useful for strict compliance scenarios. Default: $false (continue on error) .PARAMETER SkipVerification If specified, skips verification checks after rule application. Useful for speed when verification is handled separately. .PARAMETER Parallel If specified, applies rules in parallel where possible (Registry, Services). Firewall and Audit rules must run sequentially due to Windows constraints. .EXAMPLE $session = New-HardeningSession -Profile Recommended -TargetSystem Client -OSVersion 11 $result = Invoke-SecurityHardening -Session $session Applies Recommended profile hardening to local Windows 11 system. .EXAMPLE $session = New-HardeningSession -Profile Strict -TargetSystem Server -OSVersion 2022 -WhatIf $result = Invoke-SecurityHardening -Session $session $result.ComplianceReport Simulates strict hardening on Server 2022 and displays compliance report. .EXAMPLE $session = New-HardeningSession -Profile Basis -TargetSystem Client -OSVersion 11 $result = Invoke-SecurityHardening -Session $session -RuleFilter @('Account-MinimumPasswordLength', 'Firewall-EnableWindowsDefender') Applies only specific rules from Basis profile. .NOTES DEPENDENCIES: Write-Log (Core), Get-HardeningProfile (System), _ApplyHardeningRule (System) ERROR HANDLING: Logs all failures via Write-ErrorLog, continues by default LOGGING: All rule applications logged with timestamps and result codes WHATIF SUPPORT: Full WhatIf support - no changes applied in dry-run mode PARALLEL: Registry and Service rules can run in parallel; Firewall/Audit sequential #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSCustomObject] $Session, [Parameter(Mandatory = $false)] [string[]] $RuleFilter, [switch] $FailOnError, [switch] $SkipVerification, [switch] $Parallel ) begin { $ErrorActionPreference = 'Stop' } process { try { Write-Log -Message "Starting security hardening: Profile=$($Session.Profile), ComputerName=$($Session.ComputerName)" -Level Info # Validate session if ($null -eq $Session.State) { throw "Invalid session object: missing State property" } # Load profile to get rules if not already loaded if ($null -eq $Session.State.AppliedRules) { $Session.State.AppliedRules = @() $Session.State.FailedRules = @() $Session.State.SkippedRules = @() } # Get profile rules $hardeningProfile = Get-HardeningProfile -ProfileName $Session.Profile -TargetSystem $Session.TargetSystem # Filter rules if specified $rulesToApply = $hardeningProfile.Rules if ($PSBoundParameters.ContainsKey('RuleFilter')) { $rulesToApply = @($hardeningProfile.Rules | Where-Object { $_.Name -in $RuleFilter }) Write-Log -Message "Filtering to $($rulesToApply.Count) specific rules" -Level Info } $Session.State.StartTime = Get-Date # Group rules by type for parallel execution $registryRules = @($rulesToApply | Where-Object { $_.Type -eq 'Registry' }) $serviceRules = @($rulesToApply | Where-Object { $_.Type -eq 'Service' }) $firewallRules = @($rulesToApply | Where-Object { $_.Type -eq 'Firewall' }) $auditRules = @($rulesToApply | Where-Object { $_.Type -eq 'Audit' }) $otherRules = @($rulesToApply | Where-Object { $_.Type -notin @('Registry', 'Service', 'Firewall', 'Audit') }) # Apply Registry rules (can run in parallel) Write-Log -Message "Applying Registry rules: $($registryRules.Count) rules" -Level Info if ($Parallel -and $registryRules.Count -gt 1) { $registryRules | ForEach-Object -Parallel { _ApplyHardeningRule -Rule $_ -Session $using:Session -FailOnError $using:FailOnError } -ThrottleLimit 5 } else { foreach ($rule in $registryRules) { _ApplyHardeningRule -Rule $rule -Session $Session -FailOnError:$FailOnError } } # Apply Service rules (can run in parallel) Write-Log -Message "Applying Service rules: $($serviceRules.Count) rules" -Level Info if ($Parallel -and $serviceRules.Count -gt 1) { $serviceRules | ForEach-Object -Parallel { _ApplyHardeningRule -Rule $_ -Session $using:Session -FailOnError $using:FailOnError } -ThrottleLimit 5 } else { foreach ($rule in $serviceRules) { _ApplyHardeningRule -Rule $rule -Session $Session -FailOnError:$FailOnError } } # Apply Firewall rules (must be sequential due to Windows constraints) Write-Log -Message "Applying Firewall rules: $($firewallRules.Count) rules (sequential)" -Level Info foreach ($rule in $firewallRules) { _ApplyHardeningRule -Rule $rule -Session $Session -FailOnError:$FailOnError } # Apply Audit rules (must be sequential) Write-Log -Message "Applying Audit rules: $($auditRules.Count) rules (sequential)" -Level Info foreach ($rule in $auditRules) { _ApplyHardeningRule -Rule $rule -Session $Session -FailOnError:$FailOnError } # Apply other rule types Write-Log -Message "Applying other rules: $($otherRules.Count) rules" -Level Info foreach ($rule in $otherRules) { _ApplyHardeningRule -Rule $rule -Session $Session -FailOnError:$FailOnError } $Session.State.EndTime = Get-Date $Session.State.Duration = $Session.State.EndTime - $Session.State.StartTime # Generate compliance report if not skipping verification if (-not $SkipVerification) { Write-Log -Message "Verifying hardening compliance" -Level Info $Session.State.ComplianceStatus = _GenerateComplianceReport -Session $Session } else { $Session.State.ComplianceStatus = $null } # Summary $totalRules = $Session.State.TotalRules $appliedCount = @($Session.State.AppliedRules).Count $failedCount = @($Session.State.FailedRules).Count $skippedCount = @($Session.State.SkippedRules).Count Write-Log -Message "Hardening complete: Applied=$appliedCount, Failed=$failedCount, Skipped=$skippedCount, Total=$totalRules" -Level Info # Return result object $result = [ordered]@{ SessionId = $Session.SessionId Profile = $Session.Profile TargetSystem = $Session.TargetSystem ComputerName = $Session.ComputerName State = $Session.State AppliedRules = @($Session.State.AppliedRules) FailedRules = @($Session.State.FailedRules) SkippedRules = @($Session.State.SkippedRules) ComplianceReport = $Session.State.ComplianceStatus Duration = $Session.State.Duration Success = ($failedCount -eq 0) } [PSCustomObject]$result } catch { Write-ErrorLog -Message "Failed to invoke security hardening: $($_.Exception.Message)" -Caller $MyInvocation.MyCommand.Name throw } } } # ================================================================================ # Private Helper Functions # ================================================================================ function _ApplyHardeningRule { <# .SYNOPSIS Applies a single hardening rule to the system. #> [CmdletBinding()] param( [PSCustomObject]$Rule, [PSCustomObject]$Session, [switch]$FailOnError ) try { Write-Verbose "Applying rule: $($Rule.Name) (Type: $($Rule.Type))" # Check if rule should be applied in WhatIf mode if ($Session.WhatIfMode) { Write-Log -Message "[WHATIF] Would apply rule: $($Rule.Name)" -Level Info $Session.State.AppliedRules += $Rule.Name return } # Route to appropriate rule handler based on type switch ($Rule.Type) { 'Registry' { _ApplyRegistryRule -Rule $Rule } 'Service' { _ApplyServiceRule -Rule $Rule } 'Firewall' { _ApplyFirewallRule -Rule $Rule } 'Audit' { _ApplyAuditRule -Rule $Rule } 'Encryption' { _ApplyEncryptionRule -Rule $Rule } default { Write-Log -Message "Unknown rule type for $($Rule.Name): $($Rule.Type)" -Level Warning $Session.State.SkippedRules += $Rule.Name return } } Write-Log -Message "Applied rule: $($Rule.Name)" -Level Info $Session.State.AppliedRules += $Rule.Name } catch { $errorMsg = "Failed to apply rule $($Rule.Name): $($_.Exception.Message)" Write-ErrorLog -Message $errorMsg -Caller "_ApplyHardeningRule" $Session.State.FailedRules += $Rule.Name if ($FailOnError) { throw $errorMsg } } } function _ApplyRegistryRule { <# .SYNOPSIS Applies a Registry-type hardening rule. #> [CmdletBinding()] param( [PSCustomObject]$Rule ) $regDef = $Rule.RuleDefinition # Handle single registry key if ($regDef.ContainsKey('Path') -and $regDef.ContainsKey('Name')) { $path = $regDef.Path $name = $regDef.Name $value = $regDef.Value $valueType = $regDef.ValueType # Create registry path if it doesn't exist if (-not (Test-Path -Path $path)) { New-Item -Path $path -Force | Out-Null } Set-ItemProperty -Path $path -Name $name -Value $value -Type $valueType -Force Write-Log -Message "Registry: Set $path\$name = $value" -Level Info } # Handle multiple registry keys elseif ($regDef.ContainsKey('RegKeys')) { foreach ($regKey in $regDef.RegKeys) { $path = $regKey.Path $name = $regKey.Name $value = $regKey.Value if (-not (Test-Path -Path $path)) { New-Item -Path $path -Force | Out-Null } Set-ItemProperty -Path $path -Name $name -Value $value -Force } } } function _ApplyServiceRule { <# .SYNOPSIS Applies a Service-type hardening rule. #> [CmdletBinding()] param( [PSCustomObject]$Rule ) $serviceDef = $Rule.RuleDefinition # Handle feature disabling (Windows Optional Features) if ($serviceDef.ContainsKey('FeatureName')) { $featureName = $serviceDef.FeatureName $state = $serviceDef.State if ($state -eq 'Disabled') { Disable-WindowsOptionalFeature -Online -FeatureName $featureName -NoRestart -ErrorAction SilentlyContinue Write-Log -Message "Feature disabled: $featureName" -Level Info } } # Handle service startup type changes elseif ($serviceDef.ContainsKey('ServiceName')) { $serviceName = $serviceDef.ServiceName $startType = $serviceDef.StartType if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { Set-Service -Name $serviceName -StartupType $startType Write-Log -Message "Service startup type set: $serviceName = $startType" -Level Info } else { Write-Log -Message "Service not found: $serviceName" -Level Warning } } # Handle multiple services elseif ($serviceDef.ContainsKey('Services')) { foreach ($svcName in $serviceDef.Services) { if (Get-Service -Name $svcName -ErrorAction SilentlyContinue) { Set-Service -Name $svcName -StartupType $serviceDef.StartType } } } } function _ApplyFirewallRule { <# .SYNOPSIS Applies a Firewall-type hardening rule. #> [CmdletBinding()] param( [PSCustomObject]$Rule ) $fwDef = $Rule.RuleDefinition # Handle profile-level firewall settings (skip GpoBoolean type issue) if ($fwDef.ContainsKey('Profiles')) { foreach ($profile in $fwDef.Profiles) { $msg = "Firewall profile $($profile): Using default state (typically enabled)" Write-Log -Message $msg -Level Info } Write-Log -Message "Firewall profiles skipped due to GpoBoolean type constraints" -Level Warning } # Handle default policy elseif ($fwDef.ContainsKey('DefaultInboundAction')) { Set-NetFirewallProfile -Profile Domain, Private, Public ` -DefaultInboundAction $fwDef.DefaultInboundAction ` -DefaultOutboundAction $fwDef.DefaultOutboundAction -ErrorAction SilentlyContinue Write-Log -Message "Firewall default policies set" -Level Info } # Handle specific firewall rules elseif ($fwDef.ContainsKey('Name')) { $newRule = @{ DisplayName = $fwDef.Name Direction = $fwDef.Direction Action = $fwDef.Action ErrorAction = 'SilentlyContinue' } if ($fwDef.ContainsKey('Protocol')) { $newRule['Protocol'] = $fwDef.Protocol } if ($fwDef.ContainsKey('IcmpType')) { $newRule['IcmpType'] = $fwDef.IcmpType } New-NetFirewallRule @newRule Write-Log -Message "Firewall rule created: $($fwDef.Name)" -Level Info } } function _ApplyAuditRule { <# .SYNOPSIS Applies an Audit-type hardening rule using auditpol. #> [CmdletBinding()] param( [PSCustomObject]$Rule ) $auditDef = $Rule.RuleDefinition if ($auditDef.ContainsKey('SubCategory')) { $subcategory = $auditDef.SubCategory $success = if ($auditDef.Success) { 'enable' } else { 'disable' } $failure = if ($auditDef.Failure) { 'enable' } else { 'disable' } auditpol /set /subcategory:"$subcategory" /success:$success /failure:$failure 2>&1 | Out-Null Write-Log -Message "Audit policy set: $subcategory (Success=$success, Failure=$failure)" -Level Info } elseif ($auditDef.ContainsKey('Category')) { $category = $auditDef.Category $success = if ($auditDef.Success) { 'enable' } else { 'disable' } $failure = if ($auditDef.Failure) { 'enable' } else { 'disable' } auditpol /set /category:"$category" /success:$success /failure:$failure 2>&1 | Out-Null Write-Log -Message "Audit policy set: $category (Success=$success, Failure=$failure)" -Level Info } } function _ApplyEncryptionRule { <# .SYNOPSIS Applies an Encryption-type hardening rule (e.g., BitLocker). #> [CmdletBinding()] param( [PSCustomObject]$Rule ) $encDef = $Rule.RuleDefinition # Handle BitLocker if ($encDef.ContainsKey('DriveType')) { $driveType = $encDef.DriveType if ($driveType -eq 'OS') { $osVolume = Get-Volume -DriveLetter (Split-Path -Qualifier $env:SystemRoot) -ErrorAction SilentlyContinue if ($osVolume) { Enable-BitLocker -MountPoint "$($osVolume.DriveLetter):" -EncryptionMethod $encDef.EncryptionMethod ` -UsedSpaceOnly -ErrorAction SilentlyContinue | Out-Null Write-Log -Message "BitLocker enabled for OS drive" -Level Info } } } } function _GenerateComplianceReport { <# .SYNOPSIS Generates a compliance report from the hardening session. #> [CmdletBinding()] param( [PSCustomObject]$Session ) $totalRules = $Session.State.TotalRules $appliedCount = @($Session.State.AppliedRules).Count $failedCount = @($Session.State.FailedRules).Count $skippedCount = @($Session.State.SkippedRules).Count $compliancePercentage = if ($totalRules -gt 0) { [math]::Round(($appliedCount / $totalRules) * 100, 2) } else { 0 } $report = [ordered]@{ TotalRules = $totalRules AppliedRules = $appliedCount FailedRules = $failedCount SkippedRules = $skippedCount CompliancePercentage = $compliancePercentage Status = if ($failedCount -eq 0) { 'Compliant' } else { 'Non-Compliant' } ReportTime = Get-Date } [PSCustomObject]$report } |