functions/System/Drift/Get-ServiceSecurityDrift.ps1
|
function Get-ServiceSecurityDrift { <# .SYNOPSIS Detects configuration drift in Windows service security settings. .DESCRIPTION Comprehensive service security drift detection covering dangerous services, service startup types, and service account configurations. Supports profile-based configurations (Basis, Recommended, Strict) and multi-system deployment. Returns PSCustomObject array with detailed drift findings. .PARAMETER Profile Hardening profile to check against: Basis, Recommended, or Strict (default: Recommended). Profile determines which services should be disabled or running. .PARAMETER ComputerName Target computer for remote drift detection (default: localhost). For remote computers, WinRM must be enabled and user must have admin rights. .PARAMETER Credential PSCredential object for remote connection (optional, required for remote computers). .PARAMETER Detailed Switch to include detailed service information (startup type, status, account, etc.). .PARAMETER ReportDriftOnly Switch to return only services with detected drift (excludes compliant services). .EXAMPLE Get-ServiceSecurityDrift -Profile Recommended -Detailed Detects service drift against Recommended profile with detailed information. .EXAMPLE Get-ServiceSecurityDrift -ComputerName SERVER01 -Credential $cred -ReportDriftOnly Checks remote computer and returns only drifted services. .NOTES DEPENDENCIES: Write-Log (Core), Get-HardeningProfile (System) APPLIES TO: Windows Server 2016+ and Windows 11+ #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateSet('Basis', 'Recommended', 'Strict')] [string]$Profile = 'Recommended', [ValidateNotNullOrEmpty()] [string]$ComputerName = 'localhost', [System.Management.Automation.PSCredential]$Credential, [switch]$Detailed, [switch]$ReportDriftOnly ) $ErrorActionPreference = 'Stop' $findings = @() try { Write-Log -Message "Starting service security drift detection (Profile: $Profile, ComputerName: $ComputerName)" ` -Level Info -Caller $MyInvocation.MyCommand.Name # Determine target system type $targetSystem = 'Server' if ($ComputerName -eq 'localhost') { $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue if ($osInfo.ProductType -eq 1) { $targetSystem = 'Client' } } # Load hardening profile for service rules $hardeningProfile = Get-HardeningProfile -ProfileName $Profile -TargetSystem $targetSystem ` -ErrorAction SilentlyContinue if ($null -eq $hardeningProfile) { throw "Failed to load hardening profile: $Profile" } # Extract service rules from profile $serviceRules = $hardeningProfile.Rules | Where-Object { $_.Type -eq 'Service' } # Build target services list from profile rules $targetServices = @() foreach ($rule in $serviceRules) { if ($rule.RuleDefinition.ContainsKey('ServiceName')) { $targetServices += @{ Name = $rule.RuleDefinition.ServiceName Expected = $rule.RuleDefinition.StartType Severity = $rule.Severity Rule = $rule.Name } } elseif ($rule.RuleDefinition.ContainsKey('Services')) { foreach ($svc in $rule.RuleDefinition.Services) { $targetServices += @{ Name = $svc Expected = $rule.RuleDefinition.StartType Severity = $rule.Severity Rule = $rule.Name } } } } # Add critical services that should always be monitored $criticalServices = @( @{ Name = 'wuauserv'; Expected = 'Automatic'; Severity = 'CRITICAL'; Rule = 'Service-WindowsUpdate' }, @{ Name = 'WinDefend'; Expected = 'Automatic'; Severity = 'CRITICAL'; Rule = 'Service-WindowsDefender' }, @{ Name = 'mpssvc'; Expected = 'Automatic'; Severity = 'CRITICAL'; Rule = 'Service-Firewall' }, @{ Name = 'Audiosrv'; Expected = 'Manual'; Severity = 'MEDIUM'; Rule = 'Service-AudioMonitoring' }, @{ Name = 'DiagTrack'; Expected = 'Disabled'; Severity = 'HIGH'; Rule = 'Service-DiagnosticTracking' }, @{ Name = 'dmwappushservice'; Expected = 'Disabled'; Severity = 'MEDIUM'; Rule = 'Service-MobileDevice' }, @{ Name = 'TermService'; Expected = 'Manual'; Severity = 'MEDIUM'; Rule = 'Service-RemoteDesktop' } ) # Merge critical services with profile-based services (avoid duplicates) $existingNames = $targetServices.Name -as [System.Collections.Generic.HashSet[string]] foreach ($criticalService in $criticalServices) { if ($criticalService.Name -notin $existingNames) { $targetServices += $criticalService } } # Query services (local or remote) if ($PSCmdlet.ShouldProcess("Service security drift detection on $ComputerName", "Check")) { $scriptBlock = { param([string[]]$serviceNames) Get-Service -Name $serviceNames -ErrorAction SilentlyContinue | Select-Object Name, DisplayName, StartType, Status } if ($ComputerName -eq 'localhost') { $services = & $scriptBlock -serviceNames @($targetServices.Name) } else { if ($null -eq $Credential) { $services = Invoke-Command -ComputerName $ComputerName ` -ScriptBlock $scriptBlock -ArgumentList @($targetServices.Name) -ErrorAction SilentlyContinue } else { $services = Invoke-Command -ComputerName $ComputerName -Credential $Credential ` -ScriptBlock $scriptBlock -ArgumentList @($targetServices.Name) -ErrorAction SilentlyContinue } } # Analyze drift for each service foreach ($serviceConfig in $targetServices) { $service = $services | Where-Object { $_.Name -eq $serviceConfig.Name } if ($null -eq $service) { # Service not found on system $findings += [PSCustomObject]@{ Category = 'Service.Hardening' ServiceName = $serviceConfig.Name Rule = $serviceConfig.Rule Expected = $serviceConfig.Expected Actual = 'NOT_FOUND' Status = 'DRIFT' Severity = $serviceConfig.Severity Remediation = "Install or enable service: $($serviceConfig.Name)" } Write-Log -Message "Service drift: $($serviceConfig.Name) not found (expected $($serviceConfig.Expected))" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } elseif ($service.StartType -ne $serviceConfig.Expected) { # Service startup type mismatch $findings += [PSCustomObject]@{ Category = 'Service.Hardening' ServiceName = $serviceConfig.Name DisplayName = $service.DisplayName Rule = $serviceConfig.Rule Expected = $serviceConfig.Expected Actual = $service.StartType Status = 'DRIFT' Severity = $serviceConfig.Severity ComputerName = $ComputerName CurrentStatus = $service.Status Remediation = "Set-Service -Name $($serviceConfig.Name) -StartupType $($serviceConfig.Expected)" } Write-Log -Message "Service drift: $($serviceConfig.Name) startup type is $($service.StartType) (expected $($serviceConfig.Expected))" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } elseif (-not $ReportDriftOnly -and $Detailed) { # Service is compliant - include in detailed output $findings += [PSCustomObject]@{ Category = 'Service.Hardening' ServiceName = $serviceConfig.Name DisplayName = $service.DisplayName Rule = $serviceConfig.Rule Expected = $serviceConfig.Expected Actual = $service.StartType Status = 'COMPLIANT' Severity = $serviceConfig.Severity ComputerName = $ComputerName CurrentStatus = $service.Status Remediation = 'None' } } } } # Filter if ReportDriftOnly is specified if ($ReportDriftOnly) { $findings = $findings | Where-Object { $_.Status -eq 'DRIFT' } } Write-Log -Message "Service security drift detection complete: $($findings.Count) findings" ` -Level Info -Caller $MyInvocation.MyCommand.Name return $findings } catch { Write-ErrorLog -Message "Error during service security drift detection: $($_.Exception.Message)" ` -Caller $MyInvocation.MyCommand.Name throw } } |